From 6bbff1f9f3feee11bfb434cdc4cf192839d246b7 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Wed, 13 Feb 2019 15:38:14 +0100 Subject: [PATCH 01/79] Fix #63: Support for Java 11 Updated version to 0.21.0 Spring boot updated to 2.1.2.RELEASE Updated Bouncy Castle dependency --- powerauth-data-adapter/pom.xml | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/powerauth-data-adapter/pom.xml b/powerauth-data-adapter/pom.xml index e6851e67..9f3813dc 100644 --- a/powerauth-data-adapter/pom.xml +++ b/powerauth-data-adapter/pom.xml @@ -5,7 +5,7 @@ powerauth-data-adapter io.getlime.security - 0.21.0 + 0.22.0 war powerauth-data-adapter @@ -14,7 +14,7 @@ org.springframework.boot spring-boot-starter-parent - 2.0.8.RELEASE + 2.1.2.RELEASE @@ -89,12 +89,12 @@ io.getlime.security powerauth-data-adapter-model - 0.21.0 + 0.22.0 io.getlime.security powerauth-java-crypto - 0.21.0 + 0.22.0 @@ -106,7 +106,20 @@ org.bouncycastle bcprov-jdk15on - 1.60 + 1.61 + provided + + + + + javax.xml.bind + jaxb-api + 2.3.1 + + + org.glassfish.jaxb + jaxb-runtime + 2.3.1 From d6c1b71e37bd546da739782c152ad725d5e28941 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Wed, 20 Feb 2019 17:36:51 +0100 Subject: [PATCH 02/79] Fix #65: Validation fails because of missing enclosing method --- .../app/dataadapter/controller/AuthenticationController.java | 5 ++--- .../dataadapter/controller/SMSAuthorizationController.java | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java index 0cc74641..701e76fd 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java @@ -38,7 +38,6 @@ import org.springframework.web.bind.annotation.*; import javax.validation.Valid; -import java.lang.invoke.MethodHandles; /** * Controller class which handles user authentication. @@ -87,8 +86,8 @@ private void initBinder(WebDataBinder binder) { @RequestMapping(value = "/authenticate", method = RequestMethod.POST) public @ResponseBody ObjectResponse authenticate(@Valid @RequestBody ObjectRequest request, BindingResult result) throws MethodArgumentNotValidException, DataAdapterRemoteException, AuthenticationFailedException { if (result.hasErrors()) { - // Call of getEnclosingMethod() on class found using MethodHandles.lookup() returns a reference to current method - MethodParameter methodParam = new MethodParameter(MethodHandles.lookup().lookupClass().getEnclosingMethod(),0); + // Call of getEnclosingMethod() on new object returns a reference to current method + MethodParameter methodParam = new MethodParameter(new Object(){}.getClass().getEnclosingMethod(), 0); logger.warn("The authenticate request failed due to validation errors"); throw new MethodArgumentNotValidException(methodParam, result); } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java index b2c4d660..b87a8249 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java @@ -92,8 +92,8 @@ private void initBinder(WebDataBinder binder) { @RequestMapping(value = "create", method = RequestMethod.POST) public @ResponseBody ObjectResponse createAuthorizationSMS(@Valid @RequestBody ObjectRequest request, BindingResult result) throws MethodArgumentNotValidException, DataAdapterRemoteException, SMSAuthorizationFailedException, InvalidOperationContextException { if (result.hasErrors()) { - // Call of getEnclosingMethod() on class found using MethodHandles.lookup() returns a reference to current method - MethodParameter methodParam = new MethodParameter(MethodHandles.lookup().lookupClass().getEnclosingMethod(),0); + // Call of getEnclosingMethod() on new object returns a reference to current method + MethodParameter methodParam = new MethodParameter(new Object(){}.getClass().getEnclosingMethod(), 0); logger.warn("The createAuthorizationSMS request failed due to validation errors"); throw new MethodArgumentNotValidException(methodParam, result); } From e68454534acd02d21db6c5c625aeda0e45cadb76 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Wed, 20 Feb 2019 17:44:20 +0100 Subject: [PATCH 03/79] Remove unused import --- .../app/dataadapter/controller/SMSAuthorizationController.java | 1 - 1 file changed, 1 deletion(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java index b87a8249..3cf1b3f7 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java @@ -39,7 +39,6 @@ import org.springframework.web.bind.annotation.*; import javax.validation.Valid; -import java.lang.invoke.MethodHandles; /** * Controller class which handles SMS OTP authorization. From 9d5d793af0d73d02dc8cc7cce84e0a38ddce62ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20=C4=8Eurech?= Date: Wed, 27 Feb 2019 15:02:29 +0100 Subject: [PATCH 04/79] Documentation: Fixed issues found by DocuCheck tool --- docs/Customizing-Web-Flow-Appearance.md | 2 +- docs/Implementing-the-Data-Adapter-Interface.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Customizing-Web-Flow-Appearance.md b/docs/Customizing-Web-Flow-Appearance.md index 1074452e..300781c0 100644 --- a/docs/Customizing-Web-Flow-Appearance.md +++ b/docs/Customizing-Web-Flow-Appearance.md @@ -8,7 +8,7 @@ Web Flow resources which can be customized are available in the ext-resources fo The general process of updating Web Flow resources: -- Clone project [powerauth-webflow-customization](https://github.com/wultra/powerauth-webflow-customization) from GitHub. +- Clone project [powerauth-webflow-customization](https://github.com/wultra/powerauth-webflow-customization#docucheck-keep-link) from GitHub. - Update Web Flow resources by overriding existing texts, CSS, fonts and images or by adding additional resources. - When deploying Web Flow, configure the following Spring Boot property: diff --git a/docs/Implementing-the-Data-Adapter-Interface.md b/docs/Implementing-the-Data-Adapter-Interface.md index 40a61835..85b29c5d 100644 --- a/docs/Implementing-the-Data-Adapter-Interface.md +++ b/docs/Implementing-the-Data-Adapter-Interface.md @@ -36,7 +36,7 @@ Consider which of the following methods need to be implemented in your project: Implement the actual changes in Data Adapter so that it connects to an actual data source. - - Clone project [powerauth-webflow-customization](https://github.com/wultra/powerauth-webflow-customization) from GitHub. + - Clone project [powerauth-webflow-customization](https://github.com/wultra/powerauth-webflow-customization#docucheck-keep-link) from GitHub. - Update the `pom.xml` to add any required additional dependencies. - Create a proprietary client (+ client config) for your web services. - Implement the Data Adapter interface by providing your own implementation in the [DataAdapterService class](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java). You can override the sample implementation. From 778880a8d8b013ccbbfe10d9eee5e705b60c39ae Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Thu, 28 Feb 2019 20:48:19 +0100 Subject: [PATCH 05/79] Update documentation link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9ddb0888..902094e1 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ and other changes required for customizing Web Flow for clients. ## Documentation -For the most recent documentation and tutorials, please visit [PowerAuth Web Flow Customization Documentation](./docs/Home.md). +For the most recent documentation and tutorials, please visit [PowerAuth Web Flow Customization Documentation](https://developers.wultra.com/docs/latest/powerauth-webflow-customization/). ## License From 958ecaea5140942d0b0f5431d4f2d6645c3fd806 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Mon, 4 Mar 2019 13:59:47 +0100 Subject: [PATCH 06/79] Updated documentation links --- README.md | 2 +- docs/{Home.md => Readme.md} | 0 docs/_Sidebar.md | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename docs/{Home.md => Readme.md} (100%) diff --git a/README.md b/README.md index 902094e1..95b67918 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ and other changes required for customizing Web Flow for clients. ## Documentation -For the most recent documentation and tutorials, please visit [PowerAuth Web Flow Customization Documentation](https://developers.wultra.com/docs/latest/powerauth-webflow-customization/). +For the most recent documentation and tutorials, please visit [PowerAuth Web Flow Customization Documentation](https://developers.wultra.com/docs/current/powerauth-webflow-customization/). ## License diff --git a/docs/Home.md b/docs/Readme.md similarity index 100% rename from docs/Home.md rename to docs/Readme.md diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md index f1fc987e..a852b690 100644 --- a/docs/_Sidebar.md +++ b/docs/_Sidebar.md @@ -1,6 +1,6 @@ **Customizing Web Flow** -- [Home](./Home.md) +- [Home](./Readme.md) - [Customizing Web Flow Appearance](./Customizing-Web-Flow-Appearance.md) - [Implementing Data Adapter Interface](./Implementing-the-Data-Adapter-Interface.md) - [Data Adapter REST API Reference](https://github.com/wultra/powerauth-webflow/blob/develop/docs/Data-Adapter-REST-API-Reference.md) \ No newline at end of file From 5a5f45baa9604b008efc35a9d29e163fd06454c4 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Tue, 12 Mar 2019 17:46:37 +0100 Subject: [PATCH 07/79] Fix #71: Add organization context to Data Adapter API calls --- .../app/dataadapter/api/DataAdapter.java | 21 ++++++++++------ .../controller/AuthenticationController.java | 9 ++++--- .../controller/FormDataChangeController.java | 12 ++++----- .../controller/OperationChangeController.java | 6 ++--- .../SMSAuthorizationController.java | 9 +++++-- .../impl/service/DataAdapterService.java | 25 +++++++++++-------- .../AuthenticationRequestValidator.java | 6 +++++ ...reateSMSAuthorizationRequestValidator.java | 6 +++++ .../model/entity/SMSAuthorizationEntity.java | 19 ++++++++++++++ .../service/SMSPersistenceService.java | 8 +++--- 10 files changed, 85 insertions(+), 36 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java index 93df7943..188a13f6 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java @@ -35,12 +35,13 @@ public interface DataAdapter { * * @param username Username for user authentication. * @param password Password for user authentication. + * @param organizationId Organization ID. * @param operationContext Operation context. * @return UserDetailResponse Response with user details. * @throws DataAdapterRemoteException Thrown when remote communication fails. * @throws AuthenticationFailedException Thrown when authentication fails. */ - UserDetailResponse authenticateUser(String username, String password, OperationContext operationContext) throws DataAdapterRemoteException, AuthenticationFailedException; + UserDetailResponse authenticateUser(String username, String password, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, AuthenticationFailedException; /** * Fetch user detail for given user. @@ -54,59 +55,65 @@ public interface DataAdapter { /** * Decorate operation form data. * @param userId User ID. + * @param organizationId Organization ID. * @param operationContext Operation context. * @return Response with decorated operation form data * @throws DataAdapterRemoteException Thrown when remote communication fails. * @throws UserNotFoundException Thrown when user does not exist. */ - DecorateOperationFormDataResponse decorateFormData(String userId, OperationContext operationContext) throws DataAdapterRemoteException, UserNotFoundException; + DecorateOperationFormDataResponse decorateFormData(String userId, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, UserNotFoundException; /** * Receive notification about form data change. * @param userId User ID. + * @param organizationId Organization ID. * @param formDataChange Form data change. * @param operationContext Operation context. * @throws DataAdapterRemoteException Thrown when remote communication fails. */ - void formDataChangedNotification(String userId, FormDataChange formDataChange, OperationContext operationContext) throws DataAdapterRemoteException; + void formDataChangedNotification(String userId, String organizationId, FormDataChange formDataChange, OperationContext operationContext) throws DataAdapterRemoteException; /** * Receive notification about operation change. * @param userId User ID. + * @param organizationId Organization ID. * @param operationChange Operation change. * @param operationContext Operation context. * @throws DataAdapterRemoteException Thrown when remote communication fails. */ - void operationChangedNotification(String userId, OperationChange operationChange, OperationContext operationContext) throws DataAdapterRemoteException; + void operationChangedNotification(String userId, String organizationId, OperationChange operationChange, OperationContext operationContext) throws DataAdapterRemoteException; /** * Generate authorization code for SMS authorization. * @param userId User ID. + * @param organizationId Organization ID. * @param operationContext Operation context. * @return Authorization code. * @throws InvalidOperationContextException Thrown when operation context is invalid. */ - AuthorizationCode generateAuthorizationCode(String userId, OperationContext operationContext) throws InvalidOperationContextException; + AuthorizationCode generateAuthorizationCode(String userId, String organizationId, OperationContext operationContext) throws InvalidOperationContextException; /** * Generate text for SMS authorization. * @param userId User ID. + * @param organizationId Organization ID. * @param operationContext Operation context. * @param authorizationCode Authorization code. * @param lang Language for localization. * @return Generated SMS text with OTP authorization code. * @throws InvalidOperationContextException Thrown when operation context is invalid. */ - String generateSMSText(String userId, OperationContext operationContext, AuthorizationCode authorizationCode, String lang) throws InvalidOperationContextException; + String generateSMSText(String userId, String organizationId, OperationContext operationContext, AuthorizationCode authorizationCode, String lang) throws InvalidOperationContextException; /** * Send an authorization SMS with generated OTP. * @param userId User ID. + * @param organizationId Organization ID. * @param messageText Text of SMS message. * @param operationContext Operation context. * @throws DataAdapterRemoteException Thrown when remote communication fails. * @throws SMSAuthorizationFailedException Thrown when message could not be created. */ - void sendAuthorizationSMS(String userId, String messageText, OperationContext operationContext) throws DataAdapterRemoteException, SMSAuthorizationFailedException; + void sendAuthorizationSMS(String userId, String organizationId, String messageText, OperationContext operationContext) throws DataAdapterRemoteException, SMSAuthorizationFailedException; } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java index 701e76fd..02202b70 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java @@ -91,14 +91,15 @@ private void initBinder(WebDataBinder binder) { logger.warn("The authenticate request failed due to validation errors"); throw new MethodArgumentNotValidException(methodParam, result); } - logger.info("Received authenticate request, username: {}, operation ID: {}", new String[]{request.getRequestObject().getUsername(), request.getRequestObject().getOperationContext().getId()}); + logger.info("Received authenticate request, username: {}, organization ID: {}, operation ID: {}", request.getRequestObject().getUsername(), request.getRequestObject().getOrganizationId(), request.getRequestObject().getOperationContext().getId()); AuthenticationRequest authenticationRequest = request.getRequestObject(); String username = authenticationRequest.getUsername(); String password = authenticationRequest.getPassword(); + String organizationId = authenticationRequest.getOrganizationId(); OperationContext operationContext = authenticationRequest.getOperationContext(); - UserDetailResponse userDetailResponse = dataAdapter.authenticateUser(username, password, operationContext); - AuthenticationResponse response = new AuthenticationResponse(userDetailResponse.getId()); - logger.info("The authenticate request succeeded, user ID: {}, operation ID: {}", new String[]{request.getRequestObject().getUsername(), request.getRequestObject().getOperationContext().getId()}); + UserDetailResponse userDetailResponse = dataAdapter.authenticateUser(username, password, organizationId, operationContext); + AuthenticationResponse response = new AuthenticationResponse(userDetailResponse.getId(), userDetailResponse.getOrganizationId()); + logger.info("The authenticate request succeeded, user ID: {}, organization ID: {}, operation ID: {}", userDetailResponse.getId(), userDetailResponse.getOrganizationId(), request.getRequestObject().getOperationContext().getId()); return new ObjectResponse<>(response); } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/FormDataChangeController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/FormDataChangeController.java index f6eb916a..32fb3a22 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/FormDataChangeController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/FormDataChangeController.java @@ -66,13 +66,13 @@ public FormDataChangeController(DataAdapter dataAdapter) { */ @RequestMapping(value = "/change", method = RequestMethod.POST) public @ResponseBody Response formDataChangedNotification(@RequestBody ObjectRequest request) throws DataAdapterRemoteException { - logger.info("Received formDataChangedNotification request for user: {}, operation ID: {}", - new String[]{request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()}); + logger.info("Received formDataChangedNotification request for user: {}, operation ID: {}", request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); FormDataChangeNotificationRequest notification = request.getRequestObject(); String userId = notification.getUserId(); + String organizationId = notification.getOrganizationId(); OperationContext operationContext = notification.getOperationContext(); FormDataChange formDataChange = notification.getFormDataChange(); - dataAdapter.formDataChangedNotification(userId, formDataChange, operationContext); + dataAdapter.formDataChangedNotification(userId, organizationId, formDataChange, operationContext); logger.debug("The formDataChangedNotification request succeeded"); return new Response(); } @@ -87,12 +87,12 @@ public FormDataChangeController(DataAdapter dataAdapter) { */ @RequestMapping(value = "/decorate", method = RequestMethod.POST) public @ResponseBody ObjectResponse decorateOperationFormData(@RequestBody ObjectRequest request) throws DataAdapterRemoteException, UserNotFoundException { - logger.info("Received decorateOperationFormData request for user: {}, operation ID: {}", - new String[]{request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()}); + logger.info("Received decorateOperationFormData request for user: {}, operation ID: {}", request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); DecorateOperationFormDataRequest requestObject = request.getRequestObject(); String userId = requestObject.getUserId(); + String organizationId = requestObject.getOrganizationId(); OperationContext operationContext = requestObject.getOperationContext(); - DecorateOperationFormDataResponse response = dataAdapter.decorateFormData(userId, operationContext); + DecorateOperationFormDataResponse response = dataAdapter.decorateFormData(userId, organizationId, operationContext); logger.debug("The decorateOperationFormData request succeeded"); return new ObjectResponse<>(response); } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/OperationChangeController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/OperationChangeController.java index dad27586..daf614cb 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/OperationChangeController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/OperationChangeController.java @@ -62,13 +62,13 @@ public OperationChangeController(DataAdapter dataAdapter) { */ @RequestMapping(value = "/change", method = RequestMethod.POST) public @ResponseBody Response operationChangedNotification(@RequestBody ObjectRequest request) throws DataAdapterRemoteException { - logger.info("Received operationChangedNotification request for user: {}, operation ID: {}", - new String[]{request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()}); + logger.info("Received operationChangedNotification request for user: {}, operation ID: {}", request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); OperationChangeNotificationRequest notification = request.getRequestObject(); String userId = notification.getUserId(); + String organizationId = notification.getOrganizationId(); OperationContext operationContext = notification.getOperationContext(); OperationChange operationChange = notification.getOperationChange(); - dataAdapter.operationChangedNotification(userId, operationChange, operationContext); + dataAdapter.operationChangedNotification(userId, organizationId, operationChange, operationContext); logger.debug("The operationChangedNotification request succeeded"); return new Response(); } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java index 3cf1b3f7..f9d61364 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java @@ -25,6 +25,7 @@ import io.getlime.security.powerauth.app.dataadapter.impl.validation.CreateSMSAuthorizationRequestValidator; import io.getlime.security.powerauth.app.dataadapter.repository.model.entity.SMSAuthorizationEntity; import io.getlime.security.powerauth.app.dataadapter.service.SMSPersistenceService; +import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; import io.getlime.security.powerauth.lib.dataadapter.model.request.CreateSMSAuthorizationRequest; import io.getlime.security.powerauth.lib.dataadapter.model.request.VerifySMSAuthorizationRequest; import io.getlime.security.powerauth.lib.dataadapter.model.response.CreateSMSAuthorizationResponse; @@ -104,9 +105,11 @@ private void initBinder(WebDataBinder binder) { // Send SMS with generated text to target user. String userId = smsEntity.getUserId(); + String organizationId = smsEntity.getOrganizationId(); + OperationContext operationContext = smsRequest.getOperationContext(); String messageId = smsEntity.getMessageId(); String messageText = smsEntity.getMessageText(); - dataAdapter.sendAuthorizationSMS(userId, messageText, smsRequest.getOperationContext()); + dataAdapter.sendAuthorizationSMS(userId, organizationId, messageText, operationContext); // Create response. CreateSMSAuthorizationResponse response = new CreateSMSAuthorizationResponse(messageId); @@ -121,8 +124,10 @@ private void initBinder(WebDataBinder binder) { */ private SMSAuthorizationEntity createAuthorizationSMS(@Valid CreateSMSAuthorizationRequest smsRequest) throws InvalidOperationContextException { String userId = smsRequest.getUserId(); + String organizationId = smsRequest.getOrganizationId(); + OperationContext operationContext = smsRequest.getOperationContext(); String lang = smsRequest.getLang(); - return smsPersistenceService.createAuthorizationSMS(userId, smsRequest.getOperationContext(), lang); + return smsPersistenceService.createAuthorizationSMS(userId, organizationId, operationContext, lang); } /** diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index c3cc0f72..6a816c11 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -40,12 +40,15 @@ public DataAdapterService(DataAdapterI18NService dataAdapterI18NService, Operati } @Override - public UserDetailResponse authenticateUser(String username, String password, OperationContext operationContext) throws DataAdapterRemoteException, AuthenticationFailedException { + public UserDetailResponse authenticateUser(String username, String password, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, AuthenticationFailedException { // Here will be the real authentication - call to the backend providing authentication. // In case that authentication fails, throw an AuthenticationFailedException. if ("test".equals(password)) { try { - return fetchUserDetail(username); + UserDetailResponse response = fetchUserDetail(username); + // The organization needs to be set in response (e.g. client authenticated against RETAIL organization or SME organization). + response.setOrganizationId(organizationId); + return response; } catch (UserNotFoundException e) { throw new AuthenticationFailedException("login.authenticationFailed"); } @@ -73,7 +76,7 @@ public UserDetailResponse fetchUserDetail(String userId) throws DataAdapterRemot } @Override - public DecorateOperationFormDataResponse decorateFormData(String userId, OperationContext operationContext) throws DataAdapterRemoteException, UserNotFoundException { + public DecorateOperationFormDataResponse decorateFormData(String userId, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, UserNotFoundException { String operationName = operationContext.getName(); FormData formData = operationContext.getFormData(); // Fetch bank account list for given user here from the bank backend. @@ -135,31 +138,31 @@ public DecorateOperationFormDataResponse decorateFormData(String userId, Operati } @Override - public void formDataChangedNotification(String userId, FormDataChange change, OperationContext operationContext) throws DataAdapterRemoteException { + public void formDataChangedNotification(String userId, String organizationId, FormDataChange change, OperationContext operationContext) throws DataAdapterRemoteException { String operationId = operationContext.getId(); if (change instanceof BankAccountChoice) { // Handle bank account choice here (e.g. send notification to bank backend). BankAccountChoice bankAccountChoice = (BankAccountChoice) change; - logger.info("Bank account chosen: {}, operation ID: {}", new String[]{bankAccountChoice.getBankAccountId(), operationId}); + logger.info("Bank account chosen: {}, operation ID: {}", bankAccountChoice.getBankAccountId(), operationId); return; } else if (change instanceof AuthMethodChoice) { // Handle authorization method choice here (e.g. send notification to bank backend). AuthMethodChoice authMethodChoice = (AuthMethodChoice) change; - logger.info("Authorization method chosen: {}, operation ID: {}", new String[]{authMethodChoice.getChosenAuthMethod().toString(), operationId}); + logger.info("Authorization method chosen: {}, operation ID: {}", authMethodChoice.getChosenAuthMethod().toString(), operationId); return; } throw new IllegalStateException("Invalid change entity type: " + change.getType()); } @Override - public void operationChangedNotification(String userId, OperationChange change, OperationContext operationContext) throws DataAdapterRemoteException { + public void operationChangedNotification(String userId, String organizationId, OperationChange change, OperationContext operationContext) throws DataAdapterRemoteException { String operationId = operationContext.getId(); // Handle operation change here (e.g. send notification to bank backend). - logger.info("Operation changed, status: {}, operation ID: {}", new String[] {change.toString(), operationId}); + logger.info("Operation changed, status: {}, operation ID: {}", change.toString(), operationId); } @Override - public AuthorizationCode generateAuthorizationCode(String userId, OperationContext operationContext) throws InvalidOperationContextException { + public AuthorizationCode generateAuthorizationCode(String userId, String organizationId, OperationContext operationContext) throws InvalidOperationContextException { String operationName = operationContext.getName(); List digestItems = new ArrayList<>(); switch (operationName) { @@ -190,7 +193,7 @@ public AuthorizationCode generateAuthorizationCode(String userId, OperationConte } @Override - public String generateSMSText(String userId, OperationContext operationContext, AuthorizationCode authorizationCode, String lang) throws InvalidOperationContextException { + public String generateSMSText(String userId, String organizationId, OperationContext operationContext, AuthorizationCode authorizationCode, String lang) throws InvalidOperationContextException { String operationName = operationContext.getName(); String[] messageArgs; switch (operationName) { @@ -215,7 +218,7 @@ public String generateSMSText(String userId, OperationContext operationContext, } @Override - public void sendAuthorizationSMS(String userId, String messageText, OperationContext operationContext) throws DataAdapterRemoteException, SMSAuthorizationFailedException { + public void sendAuthorizationSMS(String userId, String organizationId, String messageText, OperationContext operationContext) throws DataAdapterRemoteException, SMSAuthorizationFailedException { // Add here code to send the SMS OTP message to user identified by userId with messageText. // In case message delivery fails, throw an SMSAuthorizationFailedException. } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java index 29e926bb..121cd805 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java @@ -65,6 +65,7 @@ public void validate(@Nullable Object o, @NonNull Errors errors) { // update validation logic based on the real Data Adapter requirements String username = authRequest.getUsername(); String password = authRequest.getPassword(); + String organizationId = authRequest.getOrganizationId(); OperationContext operationContext = authRequest.getOperationContext(); if (operationContext == null) { errors.rejectValue("requestObject.operationContext", "operationContext.missing"); @@ -79,6 +80,11 @@ public void validate(@Nullable Object o, @NonNull Errors errors) { errors.rejectValue("requestObject.password", "login.password.long"); } + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.organizationId", "login.organizationId.empty"); + if (username!=null && organizationId.length() > 256) { + errors.rejectValue("requestObject.organizationId", "login.organizationId.long"); + } + AuthenticationType authType = authRequest.getType(); if (authType != AuthenticationType.BASIC) { errors.rejectValue("requestObject.type", "login.type.unsupported"); diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSMSAuthorizationRequestValidator.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSMSAuthorizationRequestValidator.java index 690047a5..2ae77ef1 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSMSAuthorizationRequestValidator.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSMSAuthorizationRequestValidator.java @@ -79,6 +79,7 @@ public void validate(@Nullable Object o, @NonNull Errors errors) { // update validation logic based on the real Data Adapter requirements String userId = authRequest.getUserId(); + String organizationId = authRequest.getOrganizationId(); OperationContext operationContext = authRequest.getOperationContext(); if (operationContext == null) { errors.rejectValue("requestObject.operationContext", "operationContext.missing"); @@ -91,6 +92,11 @@ public void validate(@Nullable Object o, @NonNull Errors errors) { errors.rejectValue("requestObject.userId", "smsAuthorization.userId.long"); } + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.organizationId", "smsAuthorization.organizationId.empty"); + if (organizationId != null && organizationId.length() > 256) { + errors.rejectValue("requestObject.organizationId", "smsAuthorization.organizationId.long"); + } + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.operationContext.name", "smsAuthorization.operationName.empty"); if (operationName != null && operationName.length() > 32) { errors.rejectValue("requestObject.operationContext.name", "smsAuthorization.operationName.long"); diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/model/entity/SMSAuthorizationEntity.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/model/entity/SMSAuthorizationEntity.java index c953ed38..1dfda24a 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/model/entity/SMSAuthorizationEntity.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/model/entity/SMSAuthorizationEntity.java @@ -43,6 +43,9 @@ public class SMSAuthorizationEntity implements Serializable { @Column(name = "user_id") private String userId; + @Column(name = "organization_id") + private String organizationId; + @Column(name = "operation_name") private String operationName; @@ -118,6 +121,22 @@ public void setUserId(String userId) { this.userId = userId; } + /** + * Get organization ID. + * @return Organization ID. + */ + public String getOrganizationId() { + return organizationId; + } + + /** + * Set organization ID. + * @param organizationId Organization ID. + */ + public void setOrganizationId(String organizationId) { + this.organizationId = organizationId; + } + /** * Get operation name. * @return Operation name. diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SMSPersistenceService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SMSPersistenceService.java index 20eb9759..baeb6c1c 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SMSPersistenceService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SMSPersistenceService.java @@ -59,11 +59,12 @@ public SMSPersistenceService(DataAdapterService dataAdapterService, SMSAuthoriza /** * Create an authorization SMS message with OTP authorization code. * @param userId User ID. + * @param organizationId Organization ID. * @param operationContext Operation context. * @param lang Language for message text. * @return Created entity with SMS message details. */ - public SMSAuthorizationEntity createAuthorizationSMS(String userId, OperationContext operationContext, String lang) throws InvalidOperationContextException { + public SMSAuthorizationEntity createAuthorizationSMS(String userId, String organizationId, OperationContext operationContext, String lang) throws InvalidOperationContextException { String operationId = operationContext.getId(); String operationName = operationContext.getName(); @@ -71,15 +72,16 @@ public SMSAuthorizationEntity createAuthorizationSMS(String userId, OperationCon String messageId = UUID.randomUUID().toString(); // generate authorization code - AuthorizationCode authorizationCode = dataAdapterService.generateAuthorizationCode(userId, operationContext); + AuthorizationCode authorizationCode = dataAdapterService.generateAuthorizationCode(userId, organizationId, operationContext); // generate message text, include previously generated authorization code - String messageText = dataAdapterService.generateSMSText(userId, operationContext, authorizationCode, lang); + String messageText = dataAdapterService.generateSMSText(userId, organizationId, operationContext, authorizationCode, lang); SMSAuthorizationEntity smsEntity = new SMSAuthorizationEntity(); smsEntity.setMessageId(messageId); smsEntity.setOperationId(operationId); smsEntity.setUserId(userId); + smsEntity.setOrganizationId(organizationId); smsEntity.setOperationName(operationName); smsEntity.setAuthorizationCode(authorizationCode.getCode()); smsEntity.setSalt(authorizationCode.getSalt()); From 7990d3a67c8985a24abfbcb92f7e27595761e271 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Wed, 13 Mar 2019 15:19:58 +0100 Subject: [PATCH 08/79] Add organizationId parameter into fetchUserDetail, Data Adapter needs to be aware of the organization context --- .../powerauth/app/dataadapter/api/DataAdapter.java | 3 ++- .../dataadapter/controller/AuthenticationController.java | 7 ++++--- .../app/dataadapter/impl/service/DataAdapterService.java | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java index 188a13f6..dfe37a0a 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java @@ -46,11 +46,12 @@ public interface DataAdapter { /** * Fetch user detail for given user. * @param userId User ID. + * @param organizationId Organization ID. * @return Response with user details. * @throws DataAdapterRemoteException Thrown when remote communication fails. * @throws UserNotFoundException Thrown when user does not exist. */ - UserDetailResponse fetchUserDetail(String userId) throws DataAdapterRemoteException, UserNotFoundException; + UserDetailResponse fetchUserDetail(String userId, String organizationId) throws DataAdapterRemoteException, UserNotFoundException; /** * Decorate operation form data. diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java index 02202b70..0e478400 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java @@ -113,10 +113,11 @@ private void initBinder(WebDataBinder binder) { */ @RequestMapping(value = "/info", method = RequestMethod.POST) public @ResponseBody ObjectResponse fetchUserDetail(@RequestBody ObjectRequest request) throws DataAdapterRemoteException, UserNotFoundException { - logger.info("Received fetchUserDetail request, user ID: {}", request.getRequestObject().getId()); + logger.info("Received fetchUserDetail request, user ID: {}", request.getRequestObject().getUserId()); UserDetailRequest userDetailRequest = request.getRequestObject(); - String userId = userDetailRequest.getId(); - UserDetailResponse response = dataAdapter.fetchUserDetail(userId); + String userId = userDetailRequest.getUserId(); + String organizationId = userDetailRequest.getOrganizationId(); + UserDetailResponse response = dataAdapter.fetchUserDetail(userId, organizationId); logger.info("The fetchUserDetail request succeeded"); return new ObjectResponse<>(response); } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index 6a816c11..83348373 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -45,7 +45,7 @@ public UserDetailResponse authenticateUser(String username, String password, Str // In case that authentication fails, throw an AuthenticationFailedException. if ("test".equals(password)) { try { - UserDetailResponse response = fetchUserDetail(username); + UserDetailResponse response = fetchUserDetail(username, organizationId); // The organization needs to be set in response (e.g. client authenticated against RETAIL organization or SME organization). response.setOrganizationId(organizationId); return response; @@ -65,13 +65,14 @@ public UserDetailResponse authenticateUser(String username, String password, Str } @Override - public UserDetailResponse fetchUserDetail(String userId) throws DataAdapterRemoteException, UserNotFoundException { + public UserDetailResponse fetchUserDetail(String userId, String organizationId) throws DataAdapterRemoteException, UserNotFoundException { // Fetch user details here ... // In case that user is not found, throw a UserNotFoundException. UserDetailResponse responseObject = new UserDetailResponse(); responseObject.setId(userId); responseObject.setGivenName("John"); responseObject.setFamilyName("Doe"); + responseObject.setOrganizationId(organizationId); return responseObject; } From 0b5872e43fef45cd816301dd7eaa7b7289515c7d Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Thu, 11 Apr 2019 13:53:18 +0200 Subject: [PATCH 09/79] Fix #74: Update Spring dependencies --- powerauth-data-adapter/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powerauth-data-adapter/pom.xml b/powerauth-data-adapter/pom.xml index 9f3813dc..a23d9fcb 100644 --- a/powerauth-data-adapter/pom.xml +++ b/powerauth-data-adapter/pom.xml @@ -14,7 +14,7 @@ org.springframework.boot spring-boot-starter-parent - 2.1.2.RELEASE + 2.1.4.RELEASE From 1715e28836902b6ccb9a8bd19c745909e203a216 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Tue, 16 Apr 2019 15:09:17 +0200 Subject: [PATCH 10/79] Fix #73: Add jboss-deployment-structure.xml file --- docs/Deploying-Wildfly.md | 93 +++++++++++++++++++ docs/_Sidebar.md | 3 +- .../DataAdapterConfiguration.java | 2 + .../src/main/resources/application.properties | 3 + .../WEB-INF/jboss-deployment-structure.xml | 14 +++ 5 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 docs/Deploying-Wildfly.md create mode 100644 powerauth-data-adapter/src/main/webapp/WEB-INF/jboss-deployment-structure.xml diff --git a/docs/Deploying-Wildfly.md b/docs/Deploying-Wildfly.md new file mode 100644 index 00000000..aaf4ec57 --- /dev/null +++ b/docs/Deploying-Wildfly.md @@ -0,0 +1,93 @@ +# Deploying Data Adapter on JBoss / Wildfly + +## JBoss Deployment Descriptor + +Data Adapter contains the following configuration in `jboss-deployment-structure.xml` file for JBoss: + +``` + + + + + + + + + + + + + + +``` + +The deployment descriptor requires configuration of the `com.wultra.powerauth.data-adapter.conf` module. + +## JBoss Module for Data Adapter Configuration + +Create a new module in `PATH_TO_JBOSS/modules/system/layers/base/com/wultra/powerauth/data-adapter/conf/main`. + +The files described below should be added into this folder. + +### Main Module Configuration + +The `module.xml` configuration is used for module registration. It also adds resources from the module folder to classpath: +``` + + + + + + +``` + +### Logging Configuration + +Use the `logback.xml` file to configure logging, for example: +``` + + + + + + + + + ${LOG_FILE_DIR}/${LOG_FILE_NAME}-${INSTANCE_ID}.log + true + + ${LOG_FILE_DIR}/${LOG_FILE_NAME}-${INSTANCE_ID}-%d{yyyy-MM-dd}-%i.log + 10MB + 5 + 100MB + + + UTF-8 + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + +``` + +### Application Configuration + +The `application-ext.properties` file is used to override default configuration properties, for example: +``` +powerauth.dataAdapter.service.applicationEnvironment=TEST +``` + +Data Adapter Spring application uses the `ext` Spring profile which activates overriding of default properties by `application-ext.properties`. + +### Bouncy Castle Installation + +The Bouncy Castle module for JBoss / Wildfly needs to be enabled as a global module for Data Adapter. + +Follow the instructions in the [Installing Bouncy Castle](https://github.com/wultra/powerauth-server/blob/develop/docs/Installing-Bouncy-Castle.md) chapter of PowerAuth Server documentation. +Note that the instructions differ based on Java version and application server type. diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md index a852b690..7a2f73b5 100644 --- a/docs/_Sidebar.md +++ b/docs/_Sidebar.md @@ -3,4 +3,5 @@ - [Home](./Readme.md) - [Customizing Web Flow Appearance](./Customizing-Web-Flow-Appearance.md) - [Implementing Data Adapter Interface](./Implementing-the-Data-Adapter-Interface.md) -- [Data Adapter REST API Reference](https://github.com/wultra/powerauth-webflow/blob/develop/docs/Data-Adapter-REST-API-Reference.md) \ No newline at end of file +- [Data Adapter REST API Reference](https://github.com/wultra/powerauth-webflow/blob/develop/docs/Data-Adapter-REST-API-Reference.md) +- [Deploy Web Flow Customization on JBoss / Wildfly](./Deploying-Wildfly.md) \ No newline at end of file diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/configuration/DataAdapterConfiguration.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/configuration/DataAdapterConfiguration.java index 1bc370fc..be0ca0d2 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/configuration/DataAdapterConfiguration.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/configuration/DataAdapterConfiguration.java @@ -16,6 +16,7 @@ package io.getlime.security.powerauth.app.dataadapter.configuration; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @@ -25,6 +26,7 @@ * @author Roman Strobl, roman.strobl@wultra.com */ @Configuration +@ConfigurationProperties("ext") @ComponentScan(basePackages = {"io.getlime.security.powerauth"}) public class DataAdapterConfiguration { diff --git a/powerauth-data-adapter/src/main/resources/application.properties b/powerauth-data-adapter/src/main/resources/application.properties index 91801876..f1e31da9 100644 --- a/powerauth-data-adapter/src/main/resources/application.properties +++ b/powerauth-data-adapter/src/main/resources/application.properties @@ -1,3 +1,6 @@ +# Allow externalization of properties using application-ext.properties +spring.profiles.active=ext + # Database Keep-Alive spring.datasource.test-while-idle=true spring.datasource.test-on-borrow=true diff --git a/powerauth-data-adapter/src/main/webapp/WEB-INF/jboss-deployment-structure.xml b/powerauth-data-adapter/src/main/webapp/WEB-INF/jboss-deployment-structure.xml new file mode 100644 index 00000000..142171d6 --- /dev/null +++ b/powerauth-data-adapter/src/main/webapp/WEB-INF/jboss-deployment-structure.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + From 19d87727af34d0ae9130849bdfdda9911888cd3b Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Mon, 27 May 2019 16:20:18 +0200 Subject: [PATCH 11/79] Fix #77: Resolve advisories reported by OWASP check --- powerauth-data-adapter/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powerauth-data-adapter/pom.xml b/powerauth-data-adapter/pom.xml index a23d9fcb..ecfabe0b 100644 --- a/powerauth-data-adapter/pom.xml +++ b/powerauth-data-adapter/pom.xml @@ -14,7 +14,7 @@ org.springframework.boot spring-boot-starter-parent - 2.1.4.RELEASE + 2.1.5.RELEASE From 8bbb7ca5e47ac35c1f2103fbf41724bfe66874d2 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Mon, 27 May 2019 17:19:14 +0200 Subject: [PATCH 12/79] Fix #79: issues reported by Coverity scan --- .../app/dataadapter/controller/AuthenticationController.java | 5 +++-- .../dataadapter/controller/SMSAuthorizationController.java | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java index 701e76fd..bca207e0 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java @@ -86,8 +86,9 @@ private void initBinder(WebDataBinder binder) { @RequestMapping(value = "/authenticate", method = RequestMethod.POST) public @ResponseBody ObjectResponse authenticate(@Valid @RequestBody ObjectRequest request, BindingResult result) throws MethodArgumentNotValidException, DataAdapterRemoteException, AuthenticationFailedException { if (result.hasErrors()) { - // Call of getEnclosingMethod() on new object returns a reference to current method - MethodParameter methodParam = new MethodParameter(new Object(){}.getClass().getEnclosingMethod(), 0); + // Call of getEnclosingMethod() on local class returns a reference to current method + class Local {} + MethodParameter methodParam = new MethodParameter(Local.class.getEnclosingMethod(), 0); logger.warn("The authenticate request failed due to validation errors"); throw new MethodArgumentNotValidException(methodParam, result); } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java index 3cf1b3f7..35e59ee3 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java @@ -91,8 +91,9 @@ private void initBinder(WebDataBinder binder) { @RequestMapping(value = "create", method = RequestMethod.POST) public @ResponseBody ObjectResponse createAuthorizationSMS(@Valid @RequestBody ObjectRequest request, BindingResult result) throws MethodArgumentNotValidException, DataAdapterRemoteException, SMSAuthorizationFailedException, InvalidOperationContextException { if (result.hasErrors()) { - // Call of getEnclosingMethod() on new object returns a reference to current method - MethodParameter methodParam = new MethodParameter(new Object(){}.getClass().getEnclosingMethod(), 0); + // Call of getEnclosingMethod() on local class returns a reference to current method + class Local {} + MethodParameter methodParam = new MethodParameter(Local.class.getEnclosingMethod(), 0); logger.warn("The createAuthorizationSMS request failed due to validation errors"); throw new MethodArgumentNotValidException(methodParam, result); } From 364c58dbf78b91b415c2be8e49e53f46f07e9acc Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Mon, 10 Jun 2019 18:15:54 +0200 Subject: [PATCH 13/79] Fix #80: Prepare consent form interface and sample implementation Code cleanup --- .../app/dataadapter/api/DataAdapter.java | 43 ++++- .../controller/AuthenticationController.java | 11 +- .../controller/ConsentController.java | 162 ++++++++++++++++++ .../controller/FormDataChangeController.java | 16 +- .../controller/OperationChangeController.java | 12 +- .../SMSAuthorizationController.java | 8 +- .../controller/ServiceController.java | 7 +- .../exception/DefaultExceptionResolver.java | 16 +- .../InvalidConsentDataException.java | 58 +++++++ .../impl/service/DataAdapterService.java | 158 ++++++++++++++++- .../ConsentFormRequestValidator.java | 132 ++++++++++++++ 11 files changed, 578 insertions(+), 45 deletions(-) create mode 100644 powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ConsentController.java create mode 100644 powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/InvalidConsentDataException.java create mode 100644 powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/ConsentFormRequestValidator.java diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java index 93df7943..cc0bcb05 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java @@ -16,12 +16,10 @@ package io.getlime.security.powerauth.app.dataadapter.api; import io.getlime.security.powerauth.app.dataadapter.exception.*; -import io.getlime.security.powerauth.lib.dataadapter.model.entity.AuthorizationCode; -import io.getlime.security.powerauth.lib.dataadapter.model.entity.FormDataChange; -import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationChange; -import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; -import io.getlime.security.powerauth.lib.dataadapter.model.response.DecorateOperationFormDataResponse; -import io.getlime.security.powerauth.lib.dataadapter.model.response.UserDetailResponse; +import io.getlime.security.powerauth.lib.dataadapter.model.entity.*; +import io.getlime.security.powerauth.lib.dataadapter.model.response.*; + +import java.util.List; /** * Interface defines methods which should be implemented for integration of Web Flow with 3rd parties. @@ -109,4 +107,37 @@ public interface DataAdapter { */ void sendAuthorizationSMS(String userId, String messageText, OperationContext operationContext) throws DataAdapterRemoteException, SMSAuthorizationFailedException; + /** + * Create OAuth 2.0 consent form - prepare HTML text of consent form and add form options. + * @param userId User ID. + * @param operationContext Operation context. + * @param lang Language to use for the text of the consent form. + * @return Consent form contents with HTML text and form options. + * @throws DataAdapterRemoteException Thrown when remote communication fails. + * @throws InvalidOperationContextException Thrown when operation context is invalid. + */ + CreateConsentFormResponse createConsentForm(String userId, OperationContext operationContext, String lang) throws DataAdapterRemoteException, InvalidOperationContextException; + + /** + * Validate consent form values and generate response with validation result with optional error messages in case validation fails. + * @param userId User ID. + * @param operationContext Operation context. + * @param lang Language to use for error messages. + * @param options Options selected by the user. + * @return Consent form validation result with optional error messages in case validation fails. + * @throws DataAdapterRemoteException Thrown when remote communication fails. + * @throws InvalidOperationContextException Thrown when operation context is invalid. + */ + ValidateConsentFormResponse validateConsentForm(String userId, OperationContext operationContext, String lang, List options) throws DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException; + + /** + * Save consent form options selected by the user for an operation. + * @param userId User ID. + * @param operationContext Operation context. + * @param options Options selected by the user. + * @throws DataAdapterRemoteException Thrown when remote communication fails. + * @throws InvalidOperationContextException Thrown when operation context is invalid. + */ + SaveConsentFormResponse saveConsentForm(String userId, OperationContext operationContext, List options) throws DataAdapterRemoteException, InvalidOperationContextException; + } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java index bca207e0..d0777036 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java @@ -31,7 +31,6 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.MethodParameter; -import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.WebDataBinder; @@ -44,7 +43,7 @@ * * @author Roman Strobl, roman.strobl@wultra.com */ -@Controller +@RestController @RequestMapping("/api/auth/user") public class AuthenticationController { @@ -84,7 +83,7 @@ private void initBinder(WebDataBinder binder) { * @throws AuthenticationFailedException Thrown in case that authentication fails. */ @RequestMapping(value = "/authenticate", method = RequestMethod.POST) - public @ResponseBody ObjectResponse authenticate(@Valid @RequestBody ObjectRequest request, BindingResult result) throws MethodArgumentNotValidException, DataAdapterRemoteException, AuthenticationFailedException { + public ObjectResponse authenticate(@Valid @RequestBody ObjectRequest request, BindingResult result) throws MethodArgumentNotValidException, DataAdapterRemoteException, AuthenticationFailedException { if (result.hasErrors()) { // Call of getEnclosingMethod() on local class returns a reference to current method class Local {} @@ -92,14 +91,14 @@ class Local {} logger.warn("The authenticate request failed due to validation errors"); throw new MethodArgumentNotValidException(methodParam, result); } - logger.info("Received authenticate request, username: {}, operation ID: {}", new String[]{request.getRequestObject().getUsername(), request.getRequestObject().getOperationContext().getId()}); + logger.info("Received authenticate request, username: {}, operation ID: {}", request.getRequestObject().getUsername(), request.getRequestObject().getOperationContext().getId()); AuthenticationRequest authenticationRequest = request.getRequestObject(); String username = authenticationRequest.getUsername(); String password = authenticationRequest.getPassword(); OperationContext operationContext = authenticationRequest.getOperationContext(); UserDetailResponse userDetailResponse = dataAdapter.authenticateUser(username, password, operationContext); AuthenticationResponse response = new AuthenticationResponse(userDetailResponse.getId()); - logger.info("The authenticate request succeeded, user ID: {}, operation ID: {}", new String[]{request.getRequestObject().getUsername(), request.getRequestObject().getOperationContext().getId()}); + logger.info("The authenticate request succeeded, user ID: {}, operation ID: {}", request.getRequestObject().getUsername(), request.getRequestObject().getOperationContext().getId()); return new ObjectResponse<>(response); } @@ -112,7 +111,7 @@ class Local {} * @throws UserNotFoundException Thrown in case user is not found. */ @RequestMapping(value = "/info", method = RequestMethod.POST) - public @ResponseBody ObjectResponse fetchUserDetail(@RequestBody ObjectRequest request) throws DataAdapterRemoteException, UserNotFoundException { + public ObjectResponse fetchUserDetail(@RequestBody ObjectRequest request) throws DataAdapterRemoteException, UserNotFoundException { logger.info("Received fetchUserDetail request, user ID: {}", request.getRequestObject().getId()); UserDetailRequest userDetailRequest = request.getRequestObject(); String userId = userDetailRequest.getId(); diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ConsentController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ConsentController.java new file mode 100644 index 00000000..1316de8f --- /dev/null +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ConsentController.java @@ -0,0 +1,162 @@ +/* + * Copyright 2019 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getlime.security.powerauth.app.dataadapter.controller; + + +import io.getlime.core.rest.model.base.request.ObjectRequest; +import io.getlime.core.rest.model.base.response.ObjectResponse; +import io.getlime.security.powerauth.app.dataadapter.api.DataAdapter; +import io.getlime.security.powerauth.app.dataadapter.exception.DataAdapterRemoteException; +import io.getlime.security.powerauth.app.dataadapter.exception.InvalidConsentDataException; +import io.getlime.security.powerauth.app.dataadapter.exception.InvalidOperationContextException; +import io.getlime.security.powerauth.app.dataadapter.impl.validation.ConsentFormRequestValidator; +import io.getlime.security.powerauth.lib.dataadapter.model.entity.ConsentOption; +import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; +import io.getlime.security.powerauth.lib.dataadapter.model.request.CreateConsentFormRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.request.SaveConsentFormRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.request.ValidateConsentFormRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.response.CreateConsentFormResponse; +import io.getlime.security.powerauth.lib.dataadapter.model.response.SaveConsentFormResponse; +import io.getlime.security.powerauth.lib.dataadapter.model.response.ValidateConsentFormResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.MethodParameter; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +/** + * Controller class which handles OAuth 2.0 consent actions. + * + * @author Roman Strobl, roman.strobl@wultra.com + */ +@RestController +@RequestMapping("/api/auth/consent") +public class ConsentController { + + private static final Logger logger = LoggerFactory.getLogger(ConsentController.class); + + private final DataAdapter dataAdapter; + private final ConsentFormRequestValidator requestValidator; + + /** + * Consent controller constructor. + * @param dataAdapter Data adapter. + * @param requestValidator Request validator. + */ + @Autowired + public ConsentController(DataAdapter dataAdapter, ConsentFormRequestValidator requestValidator) { + this.dataAdapter = dataAdapter; + this.requestValidator = requestValidator; + } + + /** + * Initializes the request validator. + * @param binder Data binder. + */ + @InitBinder + private void initBinder(WebDataBinder binder) { + binder.setValidator(requestValidator); + } + + /** + * Create OAuth 2.0 consent form. + * @param request Create consent form request. + * @return Create consent form response. + * @throws DataAdapterRemoteException In case communication with remote system fails. + * @throws InvalidOperationContextException In case operation context is invalid. + */ + @RequestMapping(value = "/create", method = RequestMethod.POST) + public ObjectResponse createConsentForm(@Valid @RequestBody ObjectRequest request, BindingResult result) throws MethodArgumentNotValidException, DataAdapterRemoteException, InvalidOperationContextException { + if (result.hasErrors()) { + // Call of getEnclosingMethod() on local class returns a reference to current method + class Local {} + MethodParameter methodParam = new MethodParameter(Local.class.getEnclosingMethod(), 0); + logger.warn("The createConsentForm request failed due to validation errors"); + throw new MethodArgumentNotValidException(methodParam, result); + } + logger.info("Received createConsentForm request for user: {}, operation ID: {}", + request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); + CreateConsentFormRequest createRequest = request.getRequestObject(); + String userId = createRequest.getUserId(); + OperationContext operationContext = createRequest.getOperationContext(); + String lang = createRequest.getLang(); + CreateConsentFormResponse response = dataAdapter.createConsentForm(userId, operationContext, lang); + logger.debug("The createConsent request succeeded"); + return new ObjectResponse<>(response); + } + + /** + * Validate OAuth 2.0 consent form. + * @param request Validate consent form request. + * @return Validate consent form response. + * @throws DataAdapterRemoteException In case communication with remote system fails. + * @throws InvalidOperationContextException In case operation context is invalid. + */ + @RequestMapping(value = "/validate", method = RequestMethod.POST) + public ObjectResponse validateConsentForm(@Valid @RequestBody ObjectRequest request, BindingResult result) throws MethodArgumentNotValidException, DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException { + if (result.hasErrors()) { + // Call of getEnclosingMethod() on local class returns a reference to current method + class Local {} + MethodParameter methodParam = new MethodParameter(Local.class.getEnclosingMethod(), 0); + logger.warn("The validateConsentForm request failed due to validation errors"); + throw new MethodArgumentNotValidException(methodParam, result); + } + logger.info("Received validateConsentForm request for user: {}, operation ID: {}", + request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); + ValidateConsentFormRequest validateRequest = request.getRequestObject(); + String userId = validateRequest.getUserId(); + OperationContext operationContext = validateRequest.getOperationContext(); + String lang = validateRequest.getLang(); + List options = validateRequest.getOptions(); + ValidateConsentFormResponse response = dataAdapter.validateConsentForm(userId, operationContext, lang, options); + logger.debug("The validateConsentForm request succeeded"); + return new ObjectResponse<>(response); + } + + /** + * Save OAuth 2.0 consent form. + * @param request Save consent form request. + * @return Save consent form response. + * @throws DataAdapterRemoteException In case communication with remote system fails. + * @throws InvalidOperationContextException In case operation context is invalid. + */ + @RequestMapping(value = "/save", method = RequestMethod.POST) + public ObjectResponse saveConsentForm(@Valid @RequestBody ObjectRequest request, BindingResult result) throws MethodArgumentNotValidException, DataAdapterRemoteException, InvalidOperationContextException { + if (result.hasErrors()) { + // Call of getEnclosingMethod() on local class returns a reference to current method + class Local {} + MethodParameter methodParam = new MethodParameter(Local.class.getEnclosingMethod(), 0); + logger.warn("The saveConsentForm request failed due to validation errors"); + throw new MethodArgumentNotValidException(methodParam, result); + } + logger.info("Received saveConsentForm request for user: {}, operation ID: {}", + request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); + SaveConsentFormRequest saveRequest = request.getRequestObject(); + String userId = saveRequest.getUserId(); + OperationContext operationContext = saveRequest.getOperationContext(); + List options = saveRequest.getOptions(); + SaveConsentFormResponse response = dataAdapter.saveConsentForm(userId, operationContext, options); + logger.debug("The saveConsentForm request succeeded"); + return new ObjectResponse<>(response); + } + +} diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/FormDataChangeController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/FormDataChangeController.java index f6eb916a..064147a0 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/FormDataChangeController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/FormDataChangeController.java @@ -29,18 +29,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.*; /** * Controller class which handles notifications about changes of operation form data. * * @author Roman Strobl, roman.strobl@wultra.com */ -@Controller +@RestController @RequestMapping("/api/operation/formdata") public class FormDataChangeController { @@ -65,9 +61,9 @@ public FormDataChangeController(DataAdapter dataAdapter) { * @throws DataAdapterRemoteException Thrown in case of remote communication errors. */ @RequestMapping(value = "/change", method = RequestMethod.POST) - public @ResponseBody Response formDataChangedNotification(@RequestBody ObjectRequest request) throws DataAdapterRemoteException { + public Response formDataChangedNotification(@RequestBody ObjectRequest request) throws DataAdapterRemoteException { logger.info("Received formDataChangedNotification request for user: {}, operation ID: {}", - new String[]{request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()}); + request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); FormDataChangeNotificationRequest notification = request.getRequestObject(); String userId = notification.getUserId(); OperationContext operationContext = notification.getOperationContext(); @@ -86,9 +82,9 @@ public FormDataChangeController(DataAdapter dataAdapter) { * @throws UserNotFoundException Thrown in case user is not found. */ @RequestMapping(value = "/decorate", method = RequestMethod.POST) - public @ResponseBody ObjectResponse decorateOperationFormData(@RequestBody ObjectRequest request) throws DataAdapterRemoteException, UserNotFoundException { + public ObjectResponse decorateOperationFormData(@RequestBody ObjectRequest request) throws DataAdapterRemoteException, UserNotFoundException { logger.info("Received decorateOperationFormData request for user: {}, operation ID: {}", - new String[]{request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()}); + request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); DecorateOperationFormDataRequest requestObject = request.getRequestObject(); String userId = requestObject.getUserId(); OperationContext operationContext = requestObject.getOperationContext(); diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/OperationChangeController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/OperationChangeController.java index dad27586..bcd873c2 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/OperationChangeController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/OperationChangeController.java @@ -25,18 +25,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.*; /** * Controller class which handles notifications about changes of operation state. * * @author Roman Strobl, roman.strobl@wultra.com */ -@Controller +@RestController @RequestMapping("/api/operation") public class OperationChangeController { @@ -61,9 +57,9 @@ public OperationChangeController(DataAdapter dataAdapter) { * @throws DataAdapterRemoteException Thrown in case of remote communication errors. */ @RequestMapping(value = "/change", method = RequestMethod.POST) - public @ResponseBody Response operationChangedNotification(@RequestBody ObjectRequest request) throws DataAdapterRemoteException { + public Response operationChangedNotification(@RequestBody ObjectRequest request) throws DataAdapterRemoteException { logger.info("Received operationChangedNotification request for user: {}, operation ID: {}", - new String[]{request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()}); + request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); OperationChangeNotificationRequest notification = request.getRequestObject(); String userId = notification.getUserId(); OperationContext operationContext = notification.getOperationContext(); diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java index 35e59ee3..f57596da 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java @@ -32,7 +32,6 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.MethodParameter; -import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.WebDataBinder; @@ -45,7 +44,7 @@ * * @author Roman Strobl, roman.strobl@wultra.com */ -@Controller +@RestController @RequestMapping("/api/auth/sms") public class SMSAuthorizationController { @@ -68,7 +67,6 @@ public SMSAuthorizationController(SMSPersistenceService smsPersistenceService, C this.dataAdapter = dataAdapter; } - /** * Initializes the request validator. * @param binder Data binder. @@ -89,7 +87,7 @@ private void initBinder(WebDataBinder binder) { * @throws SMSAuthorizationFailedException Thrown in case that SMS message could not be delivered. */ @RequestMapping(value = "create", method = RequestMethod.POST) - public @ResponseBody ObjectResponse createAuthorizationSMS(@Valid @RequestBody ObjectRequest request, BindingResult result) throws MethodArgumentNotValidException, DataAdapterRemoteException, SMSAuthorizationFailedException, InvalidOperationContextException { + public ObjectResponse createAuthorizationSMS(@Valid @RequestBody ObjectRequest request, BindingResult result) throws MethodArgumentNotValidException, DataAdapterRemoteException, SMSAuthorizationFailedException, InvalidOperationContextException { if (result.hasErrors()) { // Call of getEnclosingMethod() on local class returns a reference to current method class Local {} @@ -134,7 +132,7 @@ private SMSAuthorizationEntity createAuthorizationSMS(@Valid CreateSMSAuthorizat * @throws SMSAuthorizationFailedException Thrown in case that SMS verification fails. */ @RequestMapping(value = "verify", method = RequestMethod.POST) - public @ResponseBody Response verifyAuthorizationSMS(@RequestBody ObjectRequest request) throws SMSAuthorizationFailedException { + public Response verifyAuthorizationSMS(@RequestBody ObjectRequest request) throws SMSAuthorizationFailedException { logger.info("Received verifyAuthorizationSMS request, operation ID: "+request.getRequestObject().getOperationContext().getId()); VerifySMSAuthorizationRequest verifyRequest = request.getRequestObject(); String messageId = verifyRequest.getMessageId(); diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ServiceController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ServiceController.java index cb69e724..15403db8 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ServiceController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ServiceController.java @@ -23,10 +23,9 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.info.BuildProperties; -import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; import java.util.Date; @@ -35,7 +34,7 @@ * * @author Petr Dvorak, petr@wultra.com */ -@Controller +@RestController @RequestMapping(value = "/api/service") public class ServiceController { @@ -60,7 +59,7 @@ public ServiceController(DataAdapterConfiguration dataAdapterConfiguration, Buil * @return System status info. */ @RequestMapping(value = "status", method = RequestMethod.GET) - public @ResponseBody ObjectResponse getServiceStatus() { + public ObjectResponse getServiceStatus() { logger.info("Received getServiceStatus request"); ServiceStatusResponse response = new ServiceStatusResponse(); response.setApplicationName(dataAdapterConfiguration.getApplicationName()); diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/DefaultExceptionResolver.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/DefaultExceptionResolver.java index d9e6d716..79137447 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/DefaultExceptionResolver.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/DefaultExceptionResolver.java @@ -95,7 +95,9 @@ public class DefaultExceptionResolver { List errorMessages = new ArrayList<>(); final List allErrors = ex.getBindingResult().getAllErrors(); for (ObjectError objError: allErrors) { - errorMessages.addAll(Arrays.asList(objError.getCodes())); + if (objError.getCodes() != null){ + errorMessages.addAll(Arrays.asList(objError.getCodes())); + } } // preparation of user friendly error messages for the UI @@ -163,6 +165,18 @@ public class DefaultExceptionResolver { return new ErrorResponse(error); } + /** + * Handling of invalid consent exception. + * @param ex Exception. + * @return Response with error information. + */ + @ExceptionHandler(InvalidConsentDataException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public @ResponseBody ErrorResponse handleInvalidConsentException(InvalidConsentDataException ex) { + DataAdapterError error = new DataAdapterError(DataAdapterError.Code.CONSENT_DATA_INVALID, ex.getMessage()); + return new ErrorResponse(error); + } + /** * Handling of exceptions occurring during communication with remote backends. * @param ex Exception. diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/InvalidConsentDataException.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/InvalidConsentDataException.java new file mode 100644 index 00000000..bedf19eb --- /dev/null +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/InvalidConsentDataException.java @@ -0,0 +1,58 @@ +/* + * Copyright 2017 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getlime.security.powerauth.app.dataadapter.exception; + +/** + * Exception used for case when consent data is invalid. + * + * @author Roman Strobl, roman.strobl@wultra.com + */ +public class InvalidConsentDataException extends Exception { + + /** + * Default constructor. + */ + public InvalidConsentDataException() { + } + + /** + * Constructor with message. + * + * @param message Message. + */ + public InvalidConsentDataException(String message) { + super(message); + } + + /** + * Constructor with message and cause. + * + * @param message Message. + * @param cause Cause, original exception. + */ + public InvalidConsentDataException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructor with cause. + * + * @param cause Cause, original exception. + */ + public InvalidConsentDataException(Throwable cause) { + super(cause); + } +} diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index c3cc0f72..a7566c07 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -7,8 +7,7 @@ import io.getlime.security.powerauth.lib.dataadapter.model.entity.*; import io.getlime.security.powerauth.lib.dataadapter.model.entity.attribute.AmountAttribute; import io.getlime.security.powerauth.lib.dataadapter.model.entity.attribute.FormFieldConfig; -import io.getlime.security.powerauth.lib.dataadapter.model.response.DecorateOperationFormDataResponse; -import io.getlime.security.powerauth.lib.dataadapter.model.response.UserDetailResponse; +import io.getlime.security.powerauth.lib.dataadapter.model.response.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.i18n.LocaleContextHolder; @@ -140,12 +139,12 @@ public void formDataChangedNotification(String userId, FormDataChange change, Op if (change instanceof BankAccountChoice) { // Handle bank account choice here (e.g. send notification to bank backend). BankAccountChoice bankAccountChoice = (BankAccountChoice) change; - logger.info("Bank account chosen: {}, operation ID: {}", new String[]{bankAccountChoice.getBankAccountId(), operationId}); + logger.info("Bank account chosen: {}, operation ID: {}", bankAccountChoice.getBankAccountId(), operationId); return; } else if (change instanceof AuthMethodChoice) { // Handle authorization method choice here (e.g. send notification to bank backend). AuthMethodChoice authMethodChoice = (AuthMethodChoice) change; - logger.info("Authorization method chosen: {}, operation ID: {}", new String[]{authMethodChoice.getChosenAuthMethod().toString(), operationId}); + logger.info("Authorization method chosen: {}, operation ID: {}", authMethodChoice.getChosenAuthMethod().toString(), operationId); return; } throw new IllegalStateException("Invalid change entity type: " + change.getType()); @@ -155,7 +154,7 @@ public void formDataChangedNotification(String userId, FormDataChange change, Op public void operationChangedNotification(String userId, OperationChange change, OperationContext operationContext) throws DataAdapterRemoteException { String operationId = operationContext.getId(); // Handle operation change here (e.g. send notification to bank backend). - logger.info("Operation changed, status: {}, operation ID: {}", new String[] {change.toString(), operationId}); + logger.info("Operation changed, status: {}, operation ID: {}", change.toString(), operationId); } @Override @@ -220,4 +219,153 @@ public void sendAuthorizationSMS(String userId, String messageText, OperationCo // In case message delivery fails, throw an SMSAuthorizationFailedException. } + @Override + public CreateConsentFormResponse createConsentForm(String userId, OperationContext operationContext, String lang) throws DataAdapterRemoteException, InvalidOperationContextException { + if ("login".equals(operationContext.getName())) { + // Create default consent + CreateConsentFormResponse response = new CreateConsentFormResponse(); + if ("cs".equals(lang)) { + response.setConsentHtml("Tรญmto potvrzuji, ลพe jsem inicioval tuto ลพรกdost o pล™ihlรกลกenรญ a souhlasรญm s dokonฤenรญm tรฉto operace."); + } else { + response.setConsentHtml("I consent that I have initiated this authentication request and give consent to complete the operation.

"); + } + + ConsentOption option1 = new ConsentOption(); + option1.setId("CONSENT_LOGIN"); + option1.setRequired(true); + if ("cs".equals(lang)) { + option1.setDescriptionHtml("Souhlasรญm s dokonฤenรญm operace pro pล™ihlรกลกenรญ."); + } else { + option1.setDescriptionHtml("I give consent to complete the authentication operation."); + } + + response.getOptions().add(option1); + return response; + } + if ("authorize_payment".equals(operationContext.getName())) { + CreateConsentFormResponse response = new CreateConsentFormResponse(); + if ("cs".equals(lang)) { + response.setConsentHtml("Tรญmto potvrzuji, ลพe jsem inicioval tuto platebnรญ operaci a souhlasรญm s jejรญm dokonฤenรญm."); + } else { + response.setConsentHtml("I consent that I have initiated this payment request and give consent to complete the operation."); + } + + ConsentOption option1 = new ConsentOption(); + option1.setId("CONSENT_INIT"); + option1.setRequired(true); + if ("cs".equals(lang)) { + option1.setDescriptionHtml("Potvrzuji, ลพe jsem inicioval tuto platebnรญ operaci."); + } else { + option1.setDescriptionHtml("I consent that I have initiated this payment operation."); + } + + ConsentOption option2 = new ConsentOption(); + option2.setId("CONSENT_PAYMENT"); + option2.setRequired(true); + if ("cs".equals(lang)) { + option2.setDescriptionHtml("Souhlasรญm s provedenรญm platebnรญ operace."); + } else { + option2.setDescriptionHtml("I give consent to complete this payment operation."); + } + + response.getOptions().add(option1); + response.getOptions().add(option2); + return response; + } + throw new InvalidOperationContextException("Invalid operation context"); + } + + @Override + public ValidateConsentFormResponse validateConsentForm(String userId, OperationContext operationContext, String lang, List options) throws DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException { + ValidateConsentFormResponse response = new ValidateConsentFormResponse(); + if (options == null || options.isEmpty()) { + throw new InvalidConsentDataException("Missing options for consent"); + } + if ("login".equals(operationContext.getName())) { + if (options.size() != 1) { + throw new InvalidConsentDataException("Unexpected options count for consent"); + } + // Validate default consent + if (options.get(0).getValue() == ConsentOptionValue.CHECKED) { + response.setConsentValidationPassed(true); + return response; + } + response.setConsentValidationPassed(false); + if ("cs".equals(lang)) { + response.setValidationErrorMessage("Prosรญm vyplลˆte celรฝ formulรกล™ se souhlasem."); + if (options.get(0).getValue() != ConsentOptionValue.CHECKED) { + ConsentOptionValidationResult result = new ConsentOptionValidationResult(); + result.setId("CONSENT_LOGIN"); + result.setValidationPassed(false); + result.setErrorMessage("Pro dokonฤenรญ operace odsouhlaste tuto volbu."); + response.getOptionValidationResults().add(result); + } + } else { + response.setValidationErrorMessage("Please fill in the whole consent form."); + if (options.get(0).getValue() != ConsentOptionValue.CHECKED) { + ConsentOptionValidationResult result = new ConsentOptionValidationResult(); + result.setId("CONSENT_LOGIN"); + result.setValidationPassed(false); + result.setErrorMessage("Confirm this option to complete the operation."); + response.getOptionValidationResults().add(result); + } + } + return response; + } + if ("authorize_payment".equals(operationContext.getName())) { + if (options.size() != 2) { + throw new InvalidConsentDataException("Unexpected options count for consent"); + } + if (options.get(0).getValue() == ConsentOptionValue.CHECKED && options.get(1).getValue() == ConsentOptionValue.CHECKED) { + response.setConsentValidationPassed(true); + return response; + } + response.setConsentValidationPassed(false); + if ("cs".equals(lang)) { + response.setValidationErrorMessage("Prosรญm vyplลˆte celรฝ formulรกล™ se souhlasem."); + if (options.get(0).getValue() != ConsentOptionValue.CHECKED) { + ConsentOptionValidationResult result = new ConsentOptionValidationResult(); + result.setId("CONSENT_INIT"); + result.setValidationPassed(false); + result.setErrorMessage("Pro dokonฤenรญ operace odsouhlaste tuto volbu."); + response.getOptionValidationResults().add(result); + } + if (options.get(1).getValue() != ConsentOptionValue.CHECKED) { + ConsentOptionValidationResult result = new ConsentOptionValidationResult(); + result.setId("CONSENT_PAYMENT"); + result.setValidationPassed(false); + result.setErrorMessage("Pro dokonฤenรญ operace odsouhlaste tuto volbu."); + response.getOptionValidationResults().add(result); + } + } else { + response.setValidationErrorMessage("Please fill in the whole consent form."); + if (options.get(0).getValue() != ConsentOptionValue.CHECKED) { + ConsentOptionValidationResult result = new ConsentOptionValidationResult(); + result.setId("CONSENT_INIT"); + result.setValidationPassed(false); + result.setErrorMessage("Confirm this option to complete the operation."); + response.getOptionValidationResults().add(result); + } + if (options.get(1).getValue() != ConsentOptionValue.CHECKED) { + ConsentOptionValidationResult result = new ConsentOptionValidationResult(); + result.setId("CONSENT_PAYMENT"); + result.setValidationPassed(false); + result.setErrorMessage("Confirm this option to complete the operation."); + response.getOptionValidationResults().add(result); + } + } + return response; + } + throw new InvalidOperationContextException("Invalid operation context"); + } + + @Override + public SaveConsentFormResponse saveConsentForm(String userId, OperationContext operationContext, List options) throws DataAdapterRemoteException, InvalidOperationContextException { + logger.info("Saving consent form for user: {}, operation ID: {}", userId, operationContext.getId()); + for (ConsentOption option: options) { + logger.info("Option {}: {}", option.getId(), option.getValue()); + } + return new SaveConsentFormResponse(true); + } + } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/ConsentFormRequestValidator.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/ConsentFormRequestValidator.java new file mode 100644 index 00000000..2e80f5b4 --- /dev/null +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/ConsentFormRequestValidator.java @@ -0,0 +1,132 @@ +/* + * Copyright 2017 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getlime.security.powerauth.app.dataadapter.impl.validation; + +import io.getlime.core.rest.model.base.request.ObjectRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.entity.ConsentOption; +import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; +import io.getlime.security.powerauth.lib.dataadapter.model.request.CreateConsentFormRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.request.SaveConsentFormRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.request.ValidateConsentFormRequest; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; +import org.springframework.validation.Errors; +import org.springframework.validation.ValidationUtils; +import org.springframework.validation.Validator; + +import java.util.List; + +/** + * Validator for request to create OAuth 2.0 consent form. + * + * Additional validation logic can be added if applicable. + * + * @author Roman Strobl, roman.strobl@wultra.com + */ +@Component +public class ConsentFormRequestValidator implements Validator { + + /** + * Return whether validator can validate given class. + * @param clazz Validated class. + * @return Whether validator can validate given class. + */ + @Override + public boolean supports(@NonNull Class clazz) { + return ObjectRequest.class.isAssignableFrom(clazz); + } + + /** + * Validate object and add validation errors. + * @param o Validated object. + * @param errors Errors object. + */ + @Override + @SuppressWarnings("unchecked") + public void validate(@Nullable Object o, @NonNull Errors errors) { + ObjectRequest objectRequest = (ObjectRequest) o; + if (objectRequest == null) { + errors.rejectValue("requestObject.operationContext", "operationContext.missing"); + return; + } + + // update validation logic based on the real Data Adapter requirements + if (objectRequest.getRequestObject() instanceof CreateConsentFormRequest) { + ObjectRequest requestObject = (ObjectRequest) o; + CreateConsentFormRequest request = requestObject.getRequestObject(); + validateOperationContext(request.getOperationContext(), errors); + validateUserId(request.getUserId(), errors); + if (request.getOperationContext() != null) { + validateOperationName(request.getOperationContext().getName(), errors); + } + validateLanguage(request.getLang(), errors); + } else if (objectRequest.getRequestObject() instanceof ValidateConsentFormRequest) { + ObjectRequest requestObject = (ObjectRequest) o; + ValidateConsentFormRequest request = requestObject.getRequestObject(); + validateOperationContext(request.getOperationContext(), errors); + validateUserId(request.getUserId(), errors); + if (request.getOperationContext() != null) { + validateOperationName(request.getOperationContext().getName(), errors); + } + validateLanguage(request.getLang(), errors); + validateOptions(request.getOptions(), errors); + } else if (objectRequest.getRequestObject() instanceof SaveConsentFormRequest) { + ObjectRequest requestObject = (ObjectRequest) o; + SaveConsentFormRequest request = requestObject.getRequestObject(); + validateOperationContext(request.getOperationContext(), errors); + validateUserId(request.getUserId(), errors); + if (request.getOperationContext() != null) { + validateOperationName(request.getOperationContext().getName(), errors); + } + validateOptions(request.getOptions(), errors); + } + } + + private void validateOperationContext(OperationContext operationContext, Errors errors) { + if (operationContext == null) { + errors.rejectValue("requestObject.operationContext", "operationContext.missing"); + } + } + + private void validateUserId(String userId, Errors errors) { + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.userId", "consent.invalidRequest"); + if (userId != null && userId.length() > 30) { + errors.rejectValue("requestObject.userId", "consent.invalidRequest"); + } + } + + private void validateOperationName(String operationName, Errors errors) { + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.operationContext.name", "consent.invalidRequest"); + if (operationName != null && operationName.length() > 32) { + errors.rejectValue("requestObject.operationContext.name", "consent.invalidRequest"); + } + } + + private void validateLanguage(String lang, Errors errors) { + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.lang", "consent.invalidRequest"); + if (lang != null && !lang.equals("cs") && !lang.equals("en")) { + errors.rejectValue("requestObject.lang", "consent.invalidRequest"); + } + } + + private void validateOptions(List options, Errors errors) { + ValidationUtils.rejectIfEmpty(errors, "requestObject.options", "consent.invalidRequest"); + if (options != null && options.isEmpty()) { + errors.rejectValue("requestObject.options", "consent.invalidRequest"); + } + } +} From 81a17faa3efd62665f962ef8c82f65e51446b47d Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Tue, 11 Jun 2019 18:25:19 +0200 Subject: [PATCH 14/79] Remove funky validation code --- .../app/dataadapter/api/DataAdapter.java | 4 ++- .../controller/AuthenticationController.java | 11 +------ .../controller/ConsentController.java | 29 ++++--------------- .../SMSAuthorizationController.java | 11 +------ .../impl/service/DataAdapterService.java | 2 +- 5 files changed, 11 insertions(+), 46 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java index cc0bcb05..7821d345 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java @@ -127,6 +127,7 @@ public interface DataAdapter { * @return Consent form validation result with optional error messages in case validation fails. * @throws DataAdapterRemoteException Thrown when remote communication fails. * @throws InvalidOperationContextException Thrown when operation context is invalid. + * @throws InvalidConsentDataException In case consent options are invalid. */ ValidateConsentFormResponse validateConsentForm(String userId, OperationContext operationContext, String lang, List options) throws DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException; @@ -137,7 +138,8 @@ public interface DataAdapter { * @param options Options selected by the user. * @throws DataAdapterRemoteException Thrown when remote communication fails. * @throws InvalidOperationContextException Thrown when operation context is invalid. + * @throws InvalidConsentDataException In case consent options are invalid. */ - SaveConsentFormResponse saveConsentForm(String userId, OperationContext operationContext, List options) throws DataAdapterRemoteException, InvalidOperationContextException; + SaveConsentFormResponse saveConsentForm(String userId, OperationContext operationContext, List options) throws DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException; } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java index d0777036..56c4bb51 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java @@ -76,21 +76,12 @@ private void initBinder(WebDataBinder binder) { * Authenticate user with given username and password. * * @param request Authenticate user request. - * @param result BindingResult for input validation. * @return Response with authenticated user ID. - * @throws MethodArgumentNotValidException Thrown in case form parameters are not valid. * @throws DataAdapterRemoteException Thrown in case of remote communication errors. * @throws AuthenticationFailedException Thrown in case that authentication fails. */ @RequestMapping(value = "/authenticate", method = RequestMethod.POST) - public ObjectResponse authenticate(@Valid @RequestBody ObjectRequest request, BindingResult result) throws MethodArgumentNotValidException, DataAdapterRemoteException, AuthenticationFailedException { - if (result.hasErrors()) { - // Call of getEnclosingMethod() on local class returns a reference to current method - class Local {} - MethodParameter methodParam = new MethodParameter(Local.class.getEnclosingMethod(), 0); - logger.warn("The authenticate request failed due to validation errors"); - throw new MethodArgumentNotValidException(methodParam, result); - } + public ObjectResponse authenticate(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException, AuthenticationFailedException { logger.info("Received authenticate request, username: {}, operation ID: {}", request.getRequestObject().getUsername(), request.getRequestObject().getOperationContext().getId()); AuthenticationRequest authenticationRequest = request.getRequestObject(); String username = authenticationRequest.getUsername(); diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ConsentController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ConsentController.java index 1316de8f..0c9c8334 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ConsentController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ConsentController.java @@ -85,14 +85,7 @@ private void initBinder(WebDataBinder binder) { * @throws InvalidOperationContextException In case operation context is invalid. */ @RequestMapping(value = "/create", method = RequestMethod.POST) - public ObjectResponse createConsentForm(@Valid @RequestBody ObjectRequest request, BindingResult result) throws MethodArgumentNotValidException, DataAdapterRemoteException, InvalidOperationContextException { - if (result.hasErrors()) { - // Call of getEnclosingMethod() on local class returns a reference to current method - class Local {} - MethodParameter methodParam = new MethodParameter(Local.class.getEnclosingMethod(), 0); - logger.warn("The createConsentForm request failed due to validation errors"); - throw new MethodArgumentNotValidException(methodParam, result); - } + public ObjectResponse createConsentForm(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException, InvalidOperationContextException { logger.info("Received createConsentForm request for user: {}, operation ID: {}", request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); CreateConsentFormRequest createRequest = request.getRequestObject(); @@ -110,16 +103,10 @@ class Local {} * @return Validate consent form response. * @throws DataAdapterRemoteException In case communication with remote system fails. * @throws InvalidOperationContextException In case operation context is invalid. + * @throws InvalidConsentDataException In case consent options are invalid. */ @RequestMapping(value = "/validate", method = RequestMethod.POST) - public ObjectResponse validateConsentForm(@Valid @RequestBody ObjectRequest request, BindingResult result) throws MethodArgumentNotValidException, DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException { - if (result.hasErrors()) { - // Call of getEnclosingMethod() on local class returns a reference to current method - class Local {} - MethodParameter methodParam = new MethodParameter(Local.class.getEnclosingMethod(), 0); - logger.warn("The validateConsentForm request failed due to validation errors"); - throw new MethodArgumentNotValidException(methodParam, result); - } + public ObjectResponse validateConsentForm(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException { logger.info("Received validateConsentForm request for user: {}, operation ID: {}", request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); ValidateConsentFormRequest validateRequest = request.getRequestObject(); @@ -138,16 +125,10 @@ class Local {} * @return Save consent form response. * @throws DataAdapterRemoteException In case communication with remote system fails. * @throws InvalidOperationContextException In case operation context is invalid. + * @throws InvalidConsentDataException In case consent options are invalid. */ @RequestMapping(value = "/save", method = RequestMethod.POST) - public ObjectResponse saveConsentForm(@Valid @RequestBody ObjectRequest request, BindingResult result) throws MethodArgumentNotValidException, DataAdapterRemoteException, InvalidOperationContextException { - if (result.hasErrors()) { - // Call of getEnclosingMethod() on local class returns a reference to current method - class Local {} - MethodParameter methodParam = new MethodParameter(Local.class.getEnclosingMethod(), 0); - logger.warn("The saveConsentForm request failed due to validation errors"); - throw new MethodArgumentNotValidException(methodParam, result); - } + public ObjectResponse saveConsentForm(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException { logger.info("Received saveConsentForm request for user: {}, operation ID: {}", request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); SaveConsentFormRequest saveRequest = request.getRequestObject(); diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java index f57596da..185c2970 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java @@ -80,21 +80,12 @@ private void initBinder(WebDataBinder binder) { * Create a new SMS OTP authorization message. * * @param request Request data. - * @param result BindingResult for input validation. * @return Response with message ID. - * @throws MethodArgumentNotValidException Thrown in case request is not valid. * @throws DataAdapterRemoteException Thrown in case of remote communication errors. * @throws SMSAuthorizationFailedException Thrown in case that SMS message could not be delivered. */ @RequestMapping(value = "create", method = RequestMethod.POST) - public ObjectResponse createAuthorizationSMS(@Valid @RequestBody ObjectRequest request, BindingResult result) throws MethodArgumentNotValidException, DataAdapterRemoteException, SMSAuthorizationFailedException, InvalidOperationContextException { - if (result.hasErrors()) { - // Call of getEnclosingMethod() on local class returns a reference to current method - class Local {} - MethodParameter methodParam = new MethodParameter(Local.class.getEnclosingMethod(), 0); - logger.warn("The createAuthorizationSMS request failed due to validation errors"); - throw new MethodArgumentNotValidException(methodParam, result); - } + public ObjectResponse createAuthorizationSMS(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException, SMSAuthorizationFailedException, InvalidOperationContextException { logger.info("Received createAuthorizationSMS request, operation ID: "+request.getRequestObject().getOperationContext().getId()); CreateSMSAuthorizationRequest smsRequest = request.getRequestObject(); diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index a7566c07..89d1d8c5 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -360,7 +360,7 @@ public ValidateConsentFormResponse validateConsentForm(String userId, OperationC } @Override - public SaveConsentFormResponse saveConsentForm(String userId, OperationContext operationContext, List options) throws DataAdapterRemoteException, InvalidOperationContextException { + public SaveConsentFormResponse saveConsentForm(String userId, OperationContext operationContext, List options) throws DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException { logger.info("Saving consent form for user: {}, operation ID: {}", userId, operationContext.getId()); for (ConsentOption option: options) { logger.info("Option {}: {}", option.getId(), option.getValue()); From e972242a17a455153c03db51d34cd828746500f1 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Wed, 12 Jun 2019 17:16:49 +0200 Subject: [PATCH 15/79] Document new methods for OAuth 2.0 consent screen --- ...Implementing-the-Data-Adapter-Interface.md | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/docs/Implementing-the-Data-Adapter-Interface.md b/docs/Implementing-the-Data-Adapter-Interface.md index 85b29c5d..2992a9b8 100644 --- a/docs/Implementing-the-Data-Adapter-Interface.md +++ b/docs/Implementing-the-Data-Adapter-Interface.md @@ -1,19 +1,23 @@ # Implementing the Data Adapter Interface -Data Adapter is used for connecting Web Flow to client backend systems. It allows to interact with backends for user authentication, SMS authorization, read additional data required for the operation as well as notify client backend about operation changes. +Data Adapter is used for connecting Web Flow to client backend systems. It allows to interact with backends for user authentication, SMS authorization, read additional data required for the operation as well as notify client backend about operation changes. +Furthermore, the Data Adapter can be used to customize text and options for the OAuth 2.0 consent screen. ## DataAdapter Interface The interface methods are defined in the [DataAdapter interface](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java): -- [authenticateUser](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L43) - perform user authentication with remote backend based on provided credentials -- [fetchUserDetail](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L52) - retrieve user details for given user ID -- [decorateFormData](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L62) - retrieve operation form data and decorate it -- [formDataChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L71) - method is called when operation form data changes to allow notification of client backends -- [operationChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L80) - method is called when operation status changes to allow notification of client backends -- [generateAuthorizationCode](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L89) - generate authorization code for authorization SMS message -- [generateSMSText](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L100) - generate SMS text for authorization SMS message -- [sendAuthorizationSMS](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L110) - send authorization SMS message +- [authenticateUser](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L41) - perform user authentication with remote backend based on provided credentials +- [fetchUserDetail](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L50) - retrieve user details for given user ID +- [decorateFormData](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L60) - retrieve operation form data and decorate it +- [formDataChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L69) - method is called when operation form data changes to allow notification of client backends +- [operationChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L78) - method is called when operation status changes to allow notification of client backends +- [generateAuthorizationCode](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L87) - generate authorization code for authorization SMS message +- [generateSMSText](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L98) - generate SMS text for authorization SMS message +- [sendAuthorizationSMS](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L108) - send authorization SMS message + - [createConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L119) - create an OAuth 2.0 consent form + - [validateConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L132) - validate the OAuth 2.0 consent form options + - [saveConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L143) - save the OAuth 2.0 consent form options ## Customizing Data Adapter @@ -23,14 +27,17 @@ Following steps are required for customization of Data Adapter. Consider which of the following methods need to be implemented in your project: - - [authenticateUser](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L43) (optional) - implementation is required in case any Web Flow operation needs to authenticate the user using a username/password login form - - [fetchUserDetail](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L52) (required) - provides information about the user (user ID and name) for the OAuth 2.0 protocol - - [decorateFormData](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L62) (optional) - implementation is required in case any Web Flow operation form data needs to be updated after authentication (e.g. add information about user bank accounts) - - [formDataChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L71) (optional) - implementation is required in case the client backends need to be notified about user input during an operation - - [operationChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L80) (optional) - implementation is required in case the client backends need to be notified about operation status changes - - [generateAuthorizationCode](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L89) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization - - [generateSMSText](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L100) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization - - [sendAuthorizationSMS](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L110) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization + - [authenticateUser](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L41) (optional) - implementation is required in case any Web Flow operation needs to authenticate the user using a username/password login form + - [fetchUserDetail](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L50) (required) - provides information about the user (user ID and name) for the OAuth 2.0 protocol + - [decorateFormData](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L60) (optional) - implementation is required in case any Web Flow operation form data needs to be updated after authentication (e.g. add information about user bank accounts) + - [formDataChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L69) (optional) - implementation is required in case the client backends need to be notified about user input during an operation + - [operationChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L78) (optional) - implementation is required in case the client backends need to be notified about operation status changes + - [generateAuthorizationCode](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L87) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization + - [generateSMSText](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L98) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization + - [sendAuthorizationSMS](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L108) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization + - [createConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L119) (optional) - implementation is required in case the OAuth 2.0 consent step is enabled + - [validateConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L132) (optional) - implementation is required in case the OAuth 2.0 consent step is enabled + - [saveConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L143) (optional) - implementation is required in case the OAuth 2.0 consent step is enabled ### 2. Implement the `DataAdapter` Interface From 50a6a22c72940799efeec39c3f2f6787e62781c1 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Wed, 12 Jun 2019 17:36:00 +0200 Subject: [PATCH 16/79] Documentation for OAuth 2.0 consent form customization --- docs/Customizing-Web-Flow-Appearance.md | 39 +++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/docs/Customizing-Web-Flow-Appearance.md b/docs/Customizing-Web-Flow-Appearance.md index 300781c0..c0d096e2 100644 --- a/docs/Customizing-Web-Flow-Appearance.md +++ b/docs/Customizing-Web-Flow-Appearance.md @@ -58,3 +58,42 @@ Additional fonts for Web Flow can be stored in `ext-resources/fonts` folder, see - [ext-resources/fonts](../ext-resources/fonts) After you make a copy of the `powerauth-webflow-customization` project, you can add new fonts to the folder `/path/to/your/ext-resources/fonts` and update the `customization.css` file (see above) to use the added fonts in Web Flow. + +## Customizing the OAuth 2.0 Consent Form + +The OAuth 2.0 consent form used by Web Flow can be customized by implementing following methods from Data Adapter interface: + +### Create Consent Form +The [createConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L119) method is used to specify +the text of consent form and define options which are available in the options form. The consent form accepts consent text as HTML, scripting of the HTML is not allowed. +The language of the consent form is specified using parameter `lang`. Each option is identified using an identifier `id`. Individual options in the form can be set as required and their default value can be set. +The form can use parameters `userId` and `operationContext` including `name`, `formData` and `applicationContext` to create a customized and personalized consent form for given +user, operation name, operation parameters and application which initiated the operation. + +The response should contain following data: +- `consentHtml` - localized HTML text of OAuth 2.0 consent for given operation and its context +- `options` - list of consent options which should be checked by the user with following parameters: + - `id` - identifier of the consent option + - `descriptionHtml` - localized HTML text for the description of the consent option + - `required` - whether the option must be checked in order to complete the operation + - `defaultValue` - default value of the option + - `value` - value specified by the user (not used yet) + +### Validate Consent Form +The [validateConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L132) method is used to validate the OAuth 2.0 consent form options +before the response is persisted. The identifiers of consent options match identifiers created in the `createConsentForm` step. The error messages produced by this method should +take into account language specified using parameter `lang`. + +The response should contain following data: +- `consentValidationPassed` - whether the consent validation passed and the operation can be completed +- `validationErrorMessage` - localized HTML text of error message for overall consent form validation used in case the consent validation failed +- `optionValidationResults` - result of validation for individual consent options: + - `id` - identifier of the consent option + - `validationPassed` - whether validation of the consent option passed + - `errorMessage` - localized HTML text of error message for consent option, in case validation of consent option value failed + +### Save Consent Form +The [saveConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L143) method is used to save the OAuth 2.0 consent form options. +This method is called only when form validation done in `validateConsentForm` method successfully passes. The sample implementation print the consent form option values into log. +It is expected that in the real implementation the consent option values are persisted in a database or any other persistent storage of consent options. + \ No newline at end of file From aa14497d17d44c6de57acc91776490771740f430 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Wed, 12 Jun 2019 17:38:32 +0200 Subject: [PATCH 17/79] Fix formatting --- ...Implementing-the-Data-Adapter-Interface.md | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/Implementing-the-Data-Adapter-Interface.md b/docs/Implementing-the-Data-Adapter-Interface.md index 2992a9b8..28738f25 100644 --- a/docs/Implementing-the-Data-Adapter-Interface.md +++ b/docs/Implementing-the-Data-Adapter-Interface.md @@ -15,9 +15,9 @@ The interface methods are defined in the [DataAdapter interface](../powerauth-da - [generateAuthorizationCode](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L87) - generate authorization code for authorization SMS message - [generateSMSText](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L98) - generate SMS text for authorization SMS message - [sendAuthorizationSMS](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L108) - send authorization SMS message - - [createConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L119) - create an OAuth 2.0 consent form - - [validateConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L132) - validate the OAuth 2.0 consent form options - - [saveConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L143) - save the OAuth 2.0 consent form options +- [createConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L119) - create an OAuth 2.0 consent form +- [validateConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L132) - validate the OAuth 2.0 consent form options +- [saveConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L143) - save the OAuth 2.0 consent form options ## Customizing Data Adapter @@ -27,17 +27,17 @@ Following steps are required for customization of Data Adapter. Consider which of the following methods need to be implemented in your project: - - [authenticateUser](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L41) (optional) - implementation is required in case any Web Flow operation needs to authenticate the user using a username/password login form - - [fetchUserDetail](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L50) (required) - provides information about the user (user ID and name) for the OAuth 2.0 protocol - - [decorateFormData](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L60) (optional) - implementation is required in case any Web Flow operation form data needs to be updated after authentication (e.g. add information about user bank accounts) - - [formDataChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L69) (optional) - implementation is required in case the client backends need to be notified about user input during an operation - - [operationChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L78) (optional) - implementation is required in case the client backends need to be notified about operation status changes - - [generateAuthorizationCode](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L87) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization - - [generateSMSText](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L98) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization - - [sendAuthorizationSMS](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L108) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization - - [createConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L119) (optional) - implementation is required in case the OAuth 2.0 consent step is enabled - - [validateConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L132) (optional) - implementation is required in case the OAuth 2.0 consent step is enabled - - [saveConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L143) (optional) - implementation is required in case the OAuth 2.0 consent step is enabled +- [authenticateUser](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L41) (optional) - implementation is required in case any Web Flow operation needs to authenticate the user using a username/password login form +- [fetchUserDetail](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L50) (required) - provides information about the user (user ID and name) for the OAuth 2.0 protocol +- [decorateFormData](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L60) (optional) - implementation is required in case any Web Flow operation form data needs to be updated after authentication (e.g. add information about user bank accounts) +- [formDataChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L69) (optional) - implementation is required in case the client backends need to be notified about user input during an operation +- [operationChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L78) (optional) - implementation is required in case the client backends need to be notified about operation status changes +- [generateAuthorizationCode](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L87) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization +- [generateSMSText](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L98) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization +- [sendAuthorizationSMS](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L108) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization +- [createConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L119) (optional) - implementation is required in case the OAuth 2.0 consent step is enabled +- [validateConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L132) (optional) - implementation is required in case the OAuth 2.0 consent step is enabled +- [saveConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L143) (optional) - implementation is required in case the OAuth 2.0 consent step is enabled ### 2. Implement the `DataAdapter` Interface From 879793de867f1d6da0faebb5ad104b0c99870e39 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Wed, 12 Jun 2019 17:41:51 +0200 Subject: [PATCH 18/79] Add comment about resource localization --- docs/Customizing-Web-Flow-Appearance.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/Customizing-Web-Flow-Appearance.md b/docs/Customizing-Web-Flow-Appearance.md index c0d096e2..1d1662c0 100644 --- a/docs/Customizing-Web-Flow-Appearance.md +++ b/docs/Customizing-Web-Flow-Appearance.md @@ -78,6 +78,8 @@ The response should contain following data: - `required` - whether the option must be checked in order to complete the operation - `defaultValue` - default value of the option - `value` - value specified by the user (not used yet) + +_Note that the consent texts do not use automatic resource localization because the HTML texts are expected to be complex and dynamically generated._ ### Validate Consent Form The [validateConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L132) method is used to validate the OAuth 2.0 consent form options @@ -92,6 +94,8 @@ The response should contain following data: - `validationPassed` - whether validation of the consent option passed - `errorMessage` - localized HTML text of error message for consent option, in case validation of consent option value failed +_Note that the texts of error messages do not use automatic resource localization because the HTML texts are expected to be complex and dynamically generated._ + ### Save Consent Form The [saveConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L143) method is used to save the OAuth 2.0 consent form options. This method is called only when form validation done in `validateConsentForm` method successfully passes. The sample implementation print the consent form option values into log. From 4cff8225124cfa496b6d18d4c7c39fe5b8c612ee Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Wed, 12 Jun 2019 17:42:25 +0200 Subject: [PATCH 19/79] Fixed typo --- docs/Customizing-Web-Flow-Appearance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Customizing-Web-Flow-Appearance.md b/docs/Customizing-Web-Flow-Appearance.md index 1d1662c0..4b388589 100644 --- a/docs/Customizing-Web-Flow-Appearance.md +++ b/docs/Customizing-Web-Flow-Appearance.md @@ -98,6 +98,6 @@ _Note that the texts of error messages do not use automatic resource localizatio ### Save Consent Form The [saveConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L143) method is used to save the OAuth 2.0 consent form options. -This method is called only when form validation done in `validateConsentForm` method successfully passes. The sample implementation print the consent form option values into log. +This method is called only when form validation done in `validateConsentForm` method successfully passes. The sample implementation prints the consent form option values into log. It is expected that in the real implementation the consent option values are persisted in a database or any other persistent storage of consent options. \ No newline at end of file From f4caabde204fd4b3a1ef13fbce4d61c18afcdf7a Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Thu, 13 Jun 2019 14:20:00 +0200 Subject: [PATCH 20/79] Fix imports --- .../app/dataadapter/controller/AuthenticationController.java | 3 --- .../app/dataadapter/controller/ConsentController.java | 3 --- .../app/dataadapter/controller/SMSAuthorizationController.java | 3 --- 3 files changed, 9 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java index 56c4bb51..f1c8fb1e 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java @@ -30,9 +30,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.MethodParameter; -import org.springframework.validation.BindingResult; -import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.*; diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ConsentController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ConsentController.java index 0c9c8334..cf47bba2 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ConsentController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ConsentController.java @@ -34,9 +34,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.MethodParameter; -import org.springframework.validation.BindingResult; -import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.*; diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java index 185c2970..c20a9835 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java @@ -31,9 +31,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.MethodParameter; -import org.springframework.validation.BindingResult; -import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.*; From 7492f5f197c4f5311bcb4f5afddd764e094b52bc Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Tue, 18 Jun 2019 10:01:42 +0200 Subject: [PATCH 21/79] Resolve merge conflicts --- .../app/dataadapter/controller/AuthenticationController.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java index 967394d8..2a767b63 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java @@ -79,8 +79,7 @@ private void initBinder(WebDataBinder binder) { */ @RequestMapping(value = "/authenticate", method = RequestMethod.POST) public ObjectResponse authenticate(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException, AuthenticationFailedException { - logger.info("Received authenticate request, username: {}, operation ID: {}", request.getRequestObject().getUsername(), request.getRequestObject().getOperationContext().getId()); - AuthenticationRequest authenticationRequest = request.getRequestObject(); + logger.info("Received authenticate request, username: {}, organization ID: {}, operation ID: {}", request.getRequestObject().getUsername(), request.getRequestObject().getOrganizationId(), request.getRequestObject().getOperationContext().getId()); AuthenticationRequest authenticationRequest = request.getRequestObject(); String username = authenticationRequest.getUsername(); String password = authenticationRequest.getPassword(); String organizationId = authenticationRequest.getOrganizationId(); @@ -101,7 +100,7 @@ public ObjectResponse authenticate(@Valid @RequestBody O */ @RequestMapping(value = "/info", method = RequestMethod.POST) public ObjectResponse fetchUserDetail(@RequestBody ObjectRequest request) throws DataAdapterRemoteException, UserNotFoundException { - logger.info("Received fetchUserDetail request, user ID: {}", request.getRequestObject().getId()); + logger.info("Received fetchUserDetail request, user ID: {}", request.getRequestObject().getUserId()); UserDetailRequest userDetailRequest = request.getRequestObject(); String userId = userDetailRequest.getUserId(); String organizationId = userDetailRequest.getOrganizationId(); From 7a9193ff2fd635040e31d4f59afee294946cdd58 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Tue, 18 Jun 2019 16:23:25 +0200 Subject: [PATCH 22/79] Add organizationId parameter into consent requests --- docs/Customizing-Web-Flow-Appearance.md | 2 +- .../powerauth/app/dataadapter/api/DataAdapter.java | 9 ++++++--- .../app/dataadapter/controller/ConsentController.java | 9 ++++++--- .../app/dataadapter/impl/service/DataAdapterService.java | 6 +++--- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/docs/Customizing-Web-Flow-Appearance.md b/docs/Customizing-Web-Flow-Appearance.md index 4b388589..d9c98e27 100644 --- a/docs/Customizing-Web-Flow-Appearance.md +++ b/docs/Customizing-Web-Flow-Appearance.md @@ -67,7 +67,7 @@ The OAuth 2.0 consent form used by Web Flow can be customized by implementing fo The [createConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L119) method is used to specify the text of consent form and define options which are available in the options form. The consent form accepts consent text as HTML, scripting of the HTML is not allowed. The language of the consent form is specified using parameter `lang`. Each option is identified using an identifier `id`. Individual options in the form can be set as required and their default value can be set. -The form can use parameters `userId` and `operationContext` including `name`, `formData` and `applicationContext` to create a customized and personalized consent form for given +The form can use parameters `userId`, `organizationId` and `operationContext` including `name`, `formData` and `applicationContext` to create a customized and personalized consent form for given user, operation name, operation parameters and application which initiated the operation. The response should contain following data: diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java index cd2fc4e1..47f98700 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java @@ -118,17 +118,19 @@ public interface DataAdapter { /** * Create OAuth 2.0 consent form - prepare HTML text of consent form and add form options. * @param userId User ID. + * @param organizationId Organization ID. * @param operationContext Operation context. * @param lang Language to use for the text of the consent form. * @return Consent form contents with HTML text and form options. * @throws DataAdapterRemoteException Thrown when remote communication fails. * @throws InvalidOperationContextException Thrown when operation context is invalid. */ - CreateConsentFormResponse createConsentForm(String userId, OperationContext operationContext, String lang) throws DataAdapterRemoteException, InvalidOperationContextException; + CreateConsentFormResponse createConsentForm(String userId, String organizationId, OperationContext operationContext, String lang) throws DataAdapterRemoteException, InvalidOperationContextException; /** * Validate consent form values and generate response with validation result with optional error messages in case validation fails. * @param userId User ID. + * @param organizationId Organization ID. * @param operationContext Operation context. * @param lang Language to use for error messages. * @param options Options selected by the user. @@ -137,17 +139,18 @@ public interface DataAdapter { * @throws InvalidOperationContextException Thrown when operation context is invalid. * @throws InvalidConsentDataException In case consent options are invalid. */ - ValidateConsentFormResponse validateConsentForm(String userId, OperationContext operationContext, String lang, List options) throws DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException; + ValidateConsentFormResponse validateConsentForm(String userId, String organizationId, OperationContext operationContext, String lang, List options) throws DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException; /** * Save consent form options selected by the user for an operation. * @param userId User ID. + * @param organizationId Organization ID. * @param operationContext Operation context. * @param options Options selected by the user. * @throws DataAdapterRemoteException Thrown when remote communication fails. * @throws InvalidOperationContextException Thrown when operation context is invalid. * @throws InvalidConsentDataException In case consent options are invalid. */ - SaveConsentFormResponse saveConsentForm(String userId, OperationContext operationContext, List options) throws DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException; + SaveConsentFormResponse saveConsentForm(String userId, String organizationId, OperationContext operationContext, List options) throws DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException; } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ConsentController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ConsentController.java index cf47bba2..abc64f24 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ConsentController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ConsentController.java @@ -87,9 +87,10 @@ public ObjectResponse createConsentForm(@Valid @Reque request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); CreateConsentFormRequest createRequest = request.getRequestObject(); String userId = createRequest.getUserId(); + String organizationId = createRequest.getOrganizationId(); OperationContext operationContext = createRequest.getOperationContext(); String lang = createRequest.getLang(); - CreateConsentFormResponse response = dataAdapter.createConsentForm(userId, operationContext, lang); + CreateConsentFormResponse response = dataAdapter.createConsentForm(userId, organizationId, operationContext, lang); logger.debug("The createConsent request succeeded"); return new ObjectResponse<>(response); } @@ -108,10 +109,11 @@ public ObjectResponse validateConsentForm(@Valid @R request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); ValidateConsentFormRequest validateRequest = request.getRequestObject(); String userId = validateRequest.getUserId(); + String organizationId = validateRequest.getOrganizationId(); OperationContext operationContext = validateRequest.getOperationContext(); String lang = validateRequest.getLang(); List options = validateRequest.getOptions(); - ValidateConsentFormResponse response = dataAdapter.validateConsentForm(userId, operationContext, lang, options); + ValidateConsentFormResponse response = dataAdapter.validateConsentForm(userId, organizationId, operationContext, lang, options); logger.debug("The validateConsentForm request succeeded"); return new ObjectResponse<>(response); } @@ -130,9 +132,10 @@ public ObjectResponse saveConsentForm(@Valid @RequestBo request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); SaveConsentFormRequest saveRequest = request.getRequestObject(); String userId = saveRequest.getUserId(); + String organizationId = saveRequest.getOrganizationId(); OperationContext operationContext = saveRequest.getOperationContext(); List options = saveRequest.getOptions(); - SaveConsentFormResponse response = dataAdapter.saveConsentForm(userId, operationContext, options); + SaveConsentFormResponse response = dataAdapter.saveConsentForm(userId, organizationId, operationContext, options); logger.debug("The saveConsentForm request succeeded"); return new ObjectResponse<>(response); } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index e4f7816c..3ae19a94 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -224,7 +224,7 @@ public void sendAuthorizationSMS(String userId, String organizationId, String me } @Override - public CreateConsentFormResponse createConsentForm(String userId, OperationContext operationContext, String lang) throws DataAdapterRemoteException, InvalidOperationContextException { + public CreateConsentFormResponse createConsentForm(String userId, String organizationId, OperationContext operationContext, String lang) throws DataAdapterRemoteException, InvalidOperationContextException { if ("login".equals(operationContext.getName())) { // Create default consent CreateConsentFormResponse response = new CreateConsentFormResponse(); @@ -280,7 +280,7 @@ public CreateConsentFormResponse createConsentForm(String userId, OperationConte } @Override - public ValidateConsentFormResponse validateConsentForm(String userId, OperationContext operationContext, String lang, List options) throws DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException { + public ValidateConsentFormResponse validateConsentForm(String userId, String organizationId, OperationContext operationContext, String lang, List options) throws DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException { ValidateConsentFormResponse response = new ValidateConsentFormResponse(); if (options == null || options.isEmpty()) { throw new InvalidConsentDataException("Missing options for consent"); @@ -364,7 +364,7 @@ public ValidateConsentFormResponse validateConsentForm(String userId, OperationC } @Override - public SaveConsentFormResponse saveConsentForm(String userId, OperationContext operationContext, List options) throws DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException { + public SaveConsentFormResponse saveConsentForm(String userId, String organizationId, OperationContext operationContext, List options) throws DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException { logger.info("Saving consent form for user: {}, operation ID: {}", userId, operationContext.getId()); for (ConsentOption option: options) { logger.info("Option {}: {}", option.getId(), option.getValue()); From 7d984da7f607acf76904a83bcf07a0fde1eb1d33 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Thu, 20 Jun 2019 13:16:25 +0200 Subject: [PATCH 23/79] Updated Spring boot and Jackson due to Jackson security advisory --- powerauth-data-adapter/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/powerauth-data-adapter/pom.xml b/powerauth-data-adapter/pom.xml index ecfabe0b..bbbd9652 100644 --- a/powerauth-data-adapter/pom.xml +++ b/powerauth-data-adapter/pom.xml @@ -14,7 +14,7 @@ org.springframework.boot spring-boot-starter-parent - 2.1.5.RELEASE + 2.1.6.RELEASE @@ -101,7 +101,7 @@ com.fasterxml.jackson.datatype jackson-datatype-joda - 2.9.8 + 2.9.9 org.bouncycastle From eebe48a1fac3873966d00b05bb167ab74d75f5ad Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Thu, 20 Jun 2019 18:11:10 +0200 Subject: [PATCH 24/79] Fix #83: Update Data Adapter interface for SCA and password encryption --- .../app/dataadapter/api/DataAdapter.java | 19 ++++- .../controller/AuthenticationController.java | 35 +++++++-- .../SMSAuthorizationController.java | 34 +++++++- .../impl/service/DataAdapterService.java | 18 +++-- .../AuthenticationRequestValidator.java | 77 ++++++++++++------- 5 files changed, 140 insertions(+), 43 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java index 47f98700..ef4a32e1 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java @@ -17,6 +17,7 @@ import io.getlime.security.powerauth.app.dataadapter.exception.*; import io.getlime.security.powerauth.lib.dataadapter.model.entity.*; +import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.AuthenticationType; import io.getlime.security.powerauth.lib.dataadapter.model.response.*; import java.util.List; @@ -28,28 +29,38 @@ */ public interface DataAdapter { + /** + * Lookup user account. + * @param username Username which user uses for authentication. + * @param organizationId Organization ID for this request. + * @param operationContext Operation context. + */ + UserDetailResponse lookupUser(String username, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, UserNotFoundException; + /** * Authenticate user using provided credentials. - * - * @param username Username for user authentication. + * @param userId User ID for user authentication. * @param password Password for user authentication. + * @param authenticationType Authentication type specifying optional password encryption. + * @param cipherTransformation Cipher transformation used for encryption in case password is encrypted. * @param organizationId Organization ID. * @param operationContext Operation context. * @return UserDetailResponse Response with user details. * @throws DataAdapterRemoteException Thrown when remote communication fails. * @throws AuthenticationFailedException Thrown when authentication fails. */ - UserDetailResponse authenticateUser(String username, String password, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, AuthenticationFailedException; + UserDetailResponse authenticateUser(String userId, String password, AuthenticationType authenticationType, String cipherTransformation, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, AuthenticationFailedException; /** * Fetch user detail for given user. * @param userId User ID. * @param organizationId Organization ID. + * @param operationContext Operation context which can be null in case request is initiated outside of operation scope. * @return Response with user details. * @throws DataAdapterRemoteException Thrown when remote communication fails. * @throws UserNotFoundException Thrown when user does not exist. */ - UserDetailResponse fetchUserDetail(String userId, String organizationId) throws DataAdapterRemoteException, UserNotFoundException; + UserDetailResponse fetchUserDetail(String userId, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, UserNotFoundException; /** * Decorate operation form data. diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java index 2a767b63..dd339427 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java @@ -23,8 +23,10 @@ import io.getlime.security.powerauth.app.dataadapter.exception.UserNotFoundException; import io.getlime.security.powerauth.app.dataadapter.impl.validation.AuthenticationRequestValidator; import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; +import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.AuthenticationType; import io.getlime.security.powerauth.lib.dataadapter.model.request.AuthenticationRequest; import io.getlime.security.powerauth.lib.dataadapter.model.request.UserDetailRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.request.UserLookupRequest; import io.getlime.security.powerauth.lib.dataadapter.model.response.AuthenticationResponse; import io.getlime.security.powerauth.lib.dataadapter.model.response.UserDetailResponse; import org.slf4j.Logger; @@ -70,7 +72,27 @@ private void initBinder(WebDataBinder binder) { } /** - * Authenticate user with given username and password. + * Lookup user account. + * + * @param request Lookup user account request. + * @return Response with user detail. + * @throws DataAdapterRemoteException Thrown in case of remote communication errors. + * @throws UserNotFoundException Thrown in case that user does not exist. + */ + @RequestMapping(value = "/lookup", method = RequestMethod.POST) + public ObjectResponse lookupUser(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException, UserNotFoundException { + logger.info("Received user lookup request, username: {}, organization ID: {}, operation ID: {}", request.getRequestObject().getUsername(), request.getRequestObject().getOrganizationId(), request.getRequestObject().getOperationContext().getId()); + UserLookupRequest lookupRequest = request.getRequestObject(); + String username = lookupRequest.getUsername(); + String organizationId = lookupRequest.getOrganizationId(); + OperationContext operationContext = lookupRequest.getOperationContext(); + UserDetailResponse response = dataAdapter.lookupUser(username, organizationId, operationContext); + logger.info("The user lookup request succeeded, user name: {}, organization ID: {}, operation ID: {}", response.getId(), response.getOrganizationId(), request.getRequestObject().getOperationContext().getId()); + return new ObjectResponse<>(response); + } + + /** + * Authenticate user with given credentials. * * @param request Authenticate user request. * @return Response with authenticated user ID. @@ -79,12 +101,15 @@ private void initBinder(WebDataBinder binder) { */ @RequestMapping(value = "/authenticate", method = RequestMethod.POST) public ObjectResponse authenticate(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException, AuthenticationFailedException { - logger.info("Received authenticate request, username: {}, organization ID: {}, operation ID: {}", request.getRequestObject().getUsername(), request.getRequestObject().getOrganizationId(), request.getRequestObject().getOperationContext().getId()); AuthenticationRequest authenticationRequest = request.getRequestObject(); - String username = authenticationRequest.getUsername(); + logger.info("Received authenticate request, user ID: {}, organization ID: {}, operation ID: {}", request.getRequestObject().getUserId(), request.getRequestObject().getOrganizationId(), request.getRequestObject().getOperationContext().getId()); + AuthenticationRequest authenticationRequest = request.getRequestObject(); + String userId = authenticationRequest.getUserId(); String password = authenticationRequest.getPassword(); + AuthenticationType authenticationType = authenticationRequest.getAuthenticationType(); + String cipherTransformation = authenticationRequest.getCipherTransformation(); String organizationId = authenticationRequest.getOrganizationId(); OperationContext operationContext = authenticationRequest.getOperationContext(); - UserDetailResponse userDetailResponse = dataAdapter.authenticateUser(username, password, organizationId, operationContext); + UserDetailResponse userDetailResponse = dataAdapter.authenticateUser(userId, password, authenticationType, cipherTransformation, organizationId, operationContext); AuthenticationResponse response = new AuthenticationResponse(userDetailResponse.getId(), userDetailResponse.getOrganizationId()); logger.info("The authenticate request succeeded, user ID: {}, organization ID: {}, operation ID: {}", userDetailResponse.getId(), userDetailResponse.getOrganizationId(), request.getRequestObject().getOperationContext().getId()); return new ObjectResponse<>(response); @@ -104,7 +129,7 @@ public ObjectResponse fetchUserDetail(@RequestBody ObjectReq UserDetailRequest userDetailRequest = request.getRequestObject(); String userId = userDetailRequest.getUserId(); String organizationId = userDetailRequest.getOrganizationId(); - UserDetailResponse response = dataAdapter.fetchUserDetail(userId, organizationId); + UserDetailResponse response = dataAdapter.fetchUserDetail(userId, organizationId, null); logger.info("The fetchUserDetail request succeeded"); return new ObjectResponse<>(response); } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java index 4720b0b4..7a3073db 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java @@ -19,6 +19,7 @@ import io.getlime.core.rest.model.base.response.ObjectResponse; import io.getlime.core.rest.model.base.response.Response; import io.getlime.security.powerauth.app.dataadapter.api.DataAdapter; +import io.getlime.security.powerauth.app.dataadapter.exception.AuthenticationFailedException; import io.getlime.security.powerauth.app.dataadapter.exception.DataAdapterRemoteException; import io.getlime.security.powerauth.app.dataadapter.exception.InvalidOperationContextException; import io.getlime.security.powerauth.app.dataadapter.exception.SMSAuthorizationFailedException; @@ -26,7 +27,9 @@ import io.getlime.security.powerauth.app.dataadapter.repository.model.entity.SMSAuthorizationEntity; import io.getlime.security.powerauth.app.dataadapter.service.SMSPersistenceService; import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; +import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.AuthenticationType; import io.getlime.security.powerauth.lib.dataadapter.model.request.CreateSMSAuthorizationRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.request.VerifySMSAndPasswordRequest; import io.getlime.security.powerauth.lib.dataadapter.model.request.VerifySMSAuthorizationRequest; import io.getlime.security.powerauth.lib.dataadapter.model.response.CreateSMSAuthorizationResponse; import org.slf4j.Logger; @@ -130,10 +133,39 @@ public Response verifyAuthorizationSMS(@RequestBody ObjectRequest request) throws SMSAuthorizationFailedException, AuthenticationFailedException, DataAdapterRemoteException { + logger.info("Received verifyAuthorizationSMSAndPassword request, operation ID: "+request.getRequestObject().getOperationContext().getId()); + VerifySMSAndPasswordRequest verifyRequest = request.getRequestObject(); + // Verify authorization code + String messageId = verifyRequest.getMessageId(); + String authorizationCode = verifyRequest.getAuthorizationCode(); + smsPersistenceService.verifyAuthorizationSMS(messageId, authorizationCode); + // Verify user password + String userId = verifyRequest.getUserId(); + String password = verifyRequest.getPassword(); + AuthenticationType authenticationType = verifyRequest.getAuthenticationType(); + String cipherTransformation = verifyRequest.getCipherTransformation(); + String organizationId = verifyRequest.getOrganizationId(); + OperationContext operationContext = verifyRequest.getOperationContext(); + dataAdapter.authenticateUser(userId, password, authenticationType, cipherTransformation, organizationId, operationContext); + logger.info("The verifyAuthorizationSMSAndPassword request succeeded, operation ID: "+request.getRequestObject().getOperationContext().getId()); + return new Response(); + } + } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index 3ae19a94..bc783928 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -7,6 +7,7 @@ import io.getlime.security.powerauth.lib.dataadapter.model.entity.*; import io.getlime.security.powerauth.lib.dataadapter.model.entity.attribute.AmountAttribute; import io.getlime.security.powerauth.lib.dataadapter.model.entity.attribute.FormFieldConfig; +import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.AuthenticationType; import io.getlime.security.powerauth.lib.dataadapter.model.response.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,12 +40,18 @@ public DataAdapterService(DataAdapterI18NService dataAdapterI18NService, Operati } @Override - public UserDetailResponse authenticateUser(String username, String password, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, AuthenticationFailedException { + public UserDetailResponse lookupUser(String username, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, UserNotFoundException { + // The sample Data Adapter code uses 1:1 mapping of username to userId. In real implementation the userId usually differs from the username, so translation of username to user ID is required. + return fetchUserDetail(username, organizationId, operationContext); + } + + @Override + public UserDetailResponse authenticateUser(String userId, String password, AuthenticationType authenticationType, String cipherTransformation, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, AuthenticationFailedException { // Here will be the real authentication - call to the backend providing authentication. // In case that authentication fails, throw an AuthenticationFailedException. - if ("test".equals(password)) { + if (authenticationType == AuthenticationType.BASIC && "test".equals(password)) { try { - UserDetailResponse response = fetchUserDetail(username, organizationId); + UserDetailResponse response = fetchUserDetail(userId, organizationId, operationContext); // The organization needs to be set in response (e.g. client authenticated against RETAIL organization or SME organization). response.setOrganizationId(organizationId); return response; @@ -64,9 +71,10 @@ public UserDetailResponse authenticateUser(String username, String password, Str } @Override - public UserDetailResponse fetchUserDetail(String userId, String organizationId) throws DataAdapterRemoteException, UserNotFoundException { + public UserDetailResponse fetchUserDetail(String userId, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, UserNotFoundException { // Fetch user details here ... // In case that user is not found, throw a UserNotFoundException. + // The operation context may be null in case the method is called outside of an active operation (e.g. OAuth user profile request). UserDetailResponse responseObject = new UserDetailResponse(); responseObject.setId(userId); responseObject.setGivenName("John"); @@ -218,7 +226,7 @@ public String generateSMSText(String userId, String organizationId, OperationCon } @Override - public void sendAuthorizationSMS(String userId, String organizationId, String messageText, OperationContext operationContext) throws DataAdapterRemoteException, SMSAuthorizationFailedException { + public void sendAuthorizationSMS(String userId, String organizationId, String messageText, OperationContext operationContext) throws DataAdapterRemoteException, SMSAuthorizationFailedException { // Add here code to send the SMS OTP message to user identified by userId with messageText. // In case message delivery fails, throw an SMSAuthorizationFailedException. } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java index 121cd805..40381e3a 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java @@ -19,6 +19,7 @@ import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.AuthenticationType; import io.getlime.security.powerauth.lib.dataadapter.model.request.AuthenticationRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.request.UserLookupRequest; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; @@ -27,7 +28,7 @@ import org.springframework.validation.Validator; /** - * Defines validations for input fields in authentication requests. + * Defines validations for input fields in user lookup and authentication requests. * * Additional validation logic can be added if applicable. * @@ -52,42 +53,62 @@ public boolean supports(@NonNull Class clazz) { * @param errors Errors object. */ @Override - @SuppressWarnings("unchecked") public void validate(@Nullable Object o, @NonNull Errors errors) { - ObjectRequest requestObject = (ObjectRequest) o; - if (requestObject == null) { + ObjectRequest objectRequest = (ObjectRequest) o; + if (objectRequest == null) { errors.rejectValue("requestObject.operationContext", "operationContext.missing"); return; } - AuthenticationRequest authRequest = requestObject.getRequestObject(); + if (objectRequest.getRequestObject() instanceof UserLookupRequest) { + UserLookupRequest authRequest = (UserLookupRequest) objectRequest.getRequestObject(); - // update validation logic based on the real Data Adapter requirements - String username = authRequest.getUsername(); - String password = authRequest.getPassword(); - String organizationId = authRequest.getOrganizationId(); - OperationContext operationContext = authRequest.getOperationContext(); - if (operationContext == null) { - errors.rejectValue("requestObject.operationContext", "operationContext.missing"); - } - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.username", "login.username.empty"); - if (username!=null && username.length() > 30) { - errors.rejectValue("requestObject.username", "login.username.long"); - } + // update validation logic based on the real Data Adapter requirements + String username = authRequest.getUsername(); + String organizationId = authRequest.getOrganizationId(); + OperationContext operationContext = authRequest.getOperationContext(); + if (operationContext == null) { + errors.rejectValue("requestObject.operationContext", "operationContext.missing"); + } + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.username", "login.username.empty"); + if (username!=null && username.length() > 30) { + errors.rejectValue("requestObject.username", "login.username.long"); + } - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.password", "login.password.empty"); - if (password!=null && password.length() > 30) { - errors.rejectValue("requestObject.password", "login.password.long"); - } + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.organizationId", "login.organizationId.empty"); + if (username!=null && organizationId.length() > 256) { + errors.rejectValue("requestObject.organizationId", "login.organizationId.long"); + } + } else if (objectRequest.getRequestObject() instanceof AuthenticationRequest) { + AuthenticationRequest authRequest = (AuthenticationRequest) objectRequest.getRequestObject(); - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.organizationId", "login.organizationId.empty"); - if (username!=null && organizationId.length() > 256) { - errors.rejectValue("requestObject.organizationId", "login.organizationId.long"); - } + // update validation logic based on the real Data Adapter requirements + String userId = authRequest.getUserId(); + String password = authRequest.getPassword(); + String organizationId = authRequest.getOrganizationId(); + OperationContext operationContext = authRequest.getOperationContext(); + if (operationContext == null) { + errors.rejectValue("requestObject.operationContext", "operationContext.missing"); + } + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.userId", "login.userId.empty"); + if (userId != null && userId.length() > 30) { + errors.rejectValue("requestObject.userId", "login.userId.long"); + } + + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.password", "login.password.empty"); + if (password != null && password.length() > 30) { + errors.rejectValue("requestObject.password", "login.password.long"); + } + + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.organizationId", "login.organizationId.empty"); + if (userId != null && organizationId.length() > 256) { + errors.rejectValue("requestObject.organizationId", "login.organizationId.long"); + } - AuthenticationType authType = authRequest.getType(); - if (authType != AuthenticationType.BASIC) { - errors.rejectValue("requestObject.type", "login.type.unsupported"); + AuthenticationType authType = authRequest.getAuthenticationType(); + if (authType != AuthenticationType.BASIC && authType != AuthenticationType.SYMMETRIC_PASSWORD_ENCRYPTION) { + errors.rejectValue("requestObject.authenticationType", "login.type.unsupported"); + } } } } From 2bd25d6abd2d3c29f107d159e2c9e297df96e9da Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Thu, 20 Jun 2019 18:47:16 +0200 Subject: [PATCH 25/79] Update Data Adapter interface documentation --- ...Implementing-the-Data-Adapter-Interface.md | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/docs/Implementing-the-Data-Adapter-Interface.md b/docs/Implementing-the-Data-Adapter-Interface.md index 28738f25..bebd7eeb 100644 --- a/docs/Implementing-the-Data-Adapter-Interface.md +++ b/docs/Implementing-the-Data-Adapter-Interface.md @@ -7,17 +7,18 @@ Furthermore, the Data Adapter can be used to customize text and options for the The interface methods are defined in the [DataAdapter interface](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java): -- [authenticateUser](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L41) - perform user authentication with remote backend based on provided credentials -- [fetchUserDetail](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L50) - retrieve user details for given user ID -- [decorateFormData](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L60) - retrieve operation form data and decorate it -- [formDataChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L69) - method is called when operation form data changes to allow notification of client backends -- [operationChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L78) - method is called when operation status changes to allow notification of client backends -- [generateAuthorizationCode](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L87) - generate authorization code for authorization SMS message -- [generateSMSText](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L98) - generate SMS text for authorization SMS message -- [sendAuthorizationSMS](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L108) - send authorization SMS message -- [createConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L119) - create an OAuth 2.0 consent form -- [validateConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L132) - validate the OAuth 2.0 consent form options -- [saveConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L143) - save the OAuth 2.0 consent form options +- [lookupUser](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L38) - lookup user account based on username +- [authenticateUser](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L52) - perform user authentication with remote backend based on provided credentials +- [fetchUserDetail](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L63) - retrieve user details for given user ID +- [decorateFormData](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L74) - retrieve operation form data and decorate it +- [formDataChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L84) - method is called when operation form data changes to allow notification of client backends +- [operationChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L94) - method is called when operation status changes to allow notification of client backends +- [generateAuthorizationCode](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L104) - generate authorization code for authorization SMS message +- [generateSMSText](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L116) - generate SMS text for authorization SMS message +- [sendAuthorizationSMS](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L127) - send authorization SMS message +- [createConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L139) - create an OAuth 2.0 consent form +- [validateConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L153) - validate the OAuth 2.0 consent form options +- [saveConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L165) - save the OAuth 2.0 consent form options ## Customizing Data Adapter @@ -27,17 +28,18 @@ Following steps are required for customization of Data Adapter. Consider which of the following methods need to be implemented in your project: -- [authenticateUser](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L41) (optional) - implementation is required in case any Web Flow operation needs to authenticate the user using a username/password login form -- [fetchUserDetail](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L50) (required) - provides information about the user (user ID and name) for the OAuth 2.0 protocol -- [decorateFormData](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L60) (optional) - implementation is required in case any Web Flow operation form data needs to be updated after authentication (e.g. add information about user bank accounts) -- [formDataChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L69) (optional) - implementation is required in case the client backends need to be notified about user input during an operation -- [operationChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L78) (optional) - implementation is required in case the client backends need to be notified about operation status changes -- [generateAuthorizationCode](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L87) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization -- [generateSMSText](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L98) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization -- [sendAuthorizationSMS](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L108) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization -- [createConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L119) (optional) - implementation is required in case the OAuth 2.0 consent step is enabled -- [validateConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L132) (optional) - implementation is required in case the OAuth 2.0 consent step is enabled -- [saveConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L143) (optional) - implementation is required in case the OAuth 2.0 consent step is enabled +- [lookupUser](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L38) - (required) - provides mapping of username to user ID which is used by other methods +- [authenticateUser](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L52) (optional) - implementation is required in case any Web Flow operation needs to authenticate the user using a username/password login form +- [fetchUserDetail](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L63) (required) - provides information about the user (user ID and name) for the OAuth 2.0 protocol +- [decorateFormData](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L74) (optional) - implementation is required in case any Web Flow operation form data needs to be updated after authentication (e.g. add information about user bank accounts) +- [formDataChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L84) (optional) - implementation is required in case the client backends need to be notified about user input during an operation +- [operationChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L94) (optional) - implementation is required in case the client backends need to be notified about operation status changes +- [generateAuthorizationCode](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L104) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization +- [generateSMSText](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L116) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization +- [sendAuthorizationSMS](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L127) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization +- [createConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L139) (optional) - implementation is required in case the OAuth 2.0 consent step is enabled +- [validateConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L153) (optional) - implementation is required in case the OAuth 2.0 consent step is enabled +- [saveConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L165) (optional) - implementation is required in case the OAuth 2.0 consent step is enabled ### 2. Implement the `DataAdapter` Interface From 11987ccc7920e576363b901a4ef5de1504961a64 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Fri, 21 Jun 2019 15:06:55 +0200 Subject: [PATCH 26/79] Remove 2.0 from PowerAuth --- .../src/main/resources/application.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powerauth-data-adapter/src/main/resources/application.properties b/powerauth-data-adapter/src/main/resources/application.properties index f1e31da9..cde4c112 100644 --- a/powerauth-data-adapter/src/main/resources/application.properties +++ b/powerauth-data-adapter/src/main/resources/application.properties @@ -36,7 +36,7 @@ spring.jmx.default-domain=powerauth-data-adapter # Application Service Configuration powerauth.dataAdapter.service.applicationName=powerauth-data-adapter -powerauth.dataAdapter.service.applicationDisplayName=PowerAuth 2.0 Data Adapter +powerauth.dataAdapter.service.applicationDisplayName=PowerAuth Data Adapter powerauth.dataAdapter.service.applicationEnvironment= # Disable open session in view to avoid startup warning of Spring boot From 928071d3a37ffb666c88e034e2402be162601f5c Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Fri, 21 Jun 2019 17:30:56 +0200 Subject: [PATCH 27/79] Use the new USER_NOT_FOUND error code --- .../app/dataadapter/exception/DefaultExceptionResolver.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/DefaultExceptionResolver.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/DefaultExceptionResolver.java index 79137447..32ca0a73 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/DefaultExceptionResolver.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/DefaultExceptionResolver.java @@ -149,7 +149,7 @@ public class DefaultExceptionResolver { @ExceptionHandler(UserNotFoundException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public @ResponseBody ErrorResponse handleUserNotFoundException(UserNotFoundException ex) { - DataAdapterError error = new DataAdapterError(DataAdapterError.Code.INPUT_INVALID, ex.getMessage()); + DataAdapterError error = new DataAdapterError(DataAdapterError.Code.USER_NOT_FOUND, ex.getMessage()); return new ErrorResponse(error); } From 7956ff6d58973d6929388d87a4362c058705865a Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Mon, 24 Jun 2019 15:29:50 +0200 Subject: [PATCH 28/79] Allow longer encrypted passwords in validator --- .../AuthenticationRequestValidator.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java index 40381e3a..f9d07c8e 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java @@ -95,9 +95,17 @@ public void validate(@Nullable Object o, @NonNull Errors errors) { errors.rejectValue("requestObject.userId", "login.userId.long"); } + AuthenticationType authType = authRequest.getAuthenticationType(); ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.password", "login.password.empty"); - if (password != null && password.length() > 30) { - errors.rejectValue("requestObject.password", "login.password.long"); + if (authType != AuthenticationType.BASIC) { + if (password != null && password.length() > 30) { + errors.rejectValue("requestObject.password", "login.password.long"); + } + } else { + // Allow longer values in password field when password is encrypted + if (password != null && password.length() > 256) { + errors.rejectValue("requestObject.password", "login.password.long"); + } } ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.organizationId", "login.organizationId.empty"); @@ -105,8 +113,7 @@ public void validate(@Nullable Object o, @NonNull Errors errors) { errors.rejectValue("requestObject.organizationId", "login.organizationId.long"); } - AuthenticationType authType = authRequest.getAuthenticationType(); - if (authType != AuthenticationType.BASIC && authType != AuthenticationType.SYMMETRIC_PASSWORD_ENCRYPTION) { + if (authType != AuthenticationType.BASIC && authType != AuthenticationType.PASSWORD_ENCRYPTION_AES) { errors.rejectValue("requestObject.authenticationType", "login.type.unsupported"); } } From 5fa18be552c07a60bb4535e0d1d60396876b7ec6 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Mon, 24 Jun 2019 15:48:35 +0200 Subject: [PATCH 29/79] Fix validator --- .../impl/validation/AuthenticationRequestValidator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java index f9d07c8e..f26585a3 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java @@ -97,7 +97,7 @@ public void validate(@Nullable Object o, @NonNull Errors errors) { AuthenticationType authType = authRequest.getAuthenticationType(); ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.password", "login.password.empty"); - if (authType != AuthenticationType.BASIC) { + if (authType == AuthenticationType.BASIC) { if (password != null && password.length() > 30) { errors.rejectValue("requestObject.password", "login.password.long"); } From ab59dd67ae515c3995fb11c7616f400e99935f60 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Mon, 8 Jul 2019 21:22:12 +0200 Subject: [PATCH 30/79] Fix #86: Add support for LOGIN_2FA authentication method Unify usage of camelcase in class names Fix validation of organizations --- ...Implementing-the-Data-Adapter-Interface.md | 8 +-- .../app/dataadapter/api/DataAdapter.java | 6 +- ...r.java => SmsAuthorizationController.java} | 70 +++++++++---------- .../exception/DefaultExceptionResolver.java | 4 +- ...a => SmsAuthorizationFailedException.java} | 10 +-- .../impl/service/DataAdapterService.java | 16 +++-- .../AuthenticationRequestValidator.java | 2 +- ...eateSmsAuthorizationRequestValidator.java} | 11 +-- ...y.java => SmsAuthorizationRepository.java} | 4 +- ...ntity.java => SmsAuthorizationEntity.java} | 4 +- ...ervice.java => SmsPersistenceService.java} | 47 +++++++------ .../static/resources/messages_cs.properties | 1 + .../static/resources/messages_en.properties | 1 + 13 files changed, 98 insertions(+), 86 deletions(-) rename powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/{SMSAuthorizationController.java => SmsAuthorizationController.java} (72%) rename powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/{SMSAuthorizationFailedException.java => SmsAuthorizationFailedException.java} (86%) rename powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/{CreateSMSAuthorizationRequestValidator.java => CreateSmsAuthorizationRequestValidator.java} (94%) rename powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/{SMSAuthorizationRepository.java => SmsAuthorizationRepository.java} (87%) rename powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/model/entity/{SMSAuthorizationEntity.java => SmsAuthorizationEntity.java} (98%) rename powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/{SMSPersistenceService.java => SmsPersistenceService.java} (76%) diff --git a/docs/Implementing-the-Data-Adapter-Interface.md b/docs/Implementing-the-Data-Adapter-Interface.md index bebd7eeb..a0c3e3e7 100644 --- a/docs/Implementing-the-Data-Adapter-Interface.md +++ b/docs/Implementing-the-Data-Adapter-Interface.md @@ -14,8 +14,8 @@ The interface methods are defined in the [DataAdapter interface](../powerauth-da - [formDataChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L84) - method is called when operation form data changes to allow notification of client backends - [operationChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L94) - method is called when operation status changes to allow notification of client backends - [generateAuthorizationCode](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L104) - generate authorization code for authorization SMS message -- [generateSMSText](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L116) - generate SMS text for authorization SMS message -- [sendAuthorizationSMS](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L127) - send authorization SMS message +- [generateSmsText](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L116) - generate SMS text for authorization SMS message +- [sendAuthorizationSms](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L127) - send authorization SMS message - [createConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L139) - create an OAuth 2.0 consent form - [validateConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L153) - validate the OAuth 2.0 consent form options - [saveConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L165) - save the OAuth 2.0 consent form options @@ -35,8 +35,8 @@ Consider which of the following methods need to be implemented in your project: - [formDataChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L84) (optional) - implementation is required in case the client backends need to be notified about user input during an operation - [operationChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L94) (optional) - implementation is required in case the client backends need to be notified about operation status changes - [generateAuthorizationCode](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L104) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization -- [generateSMSText](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L116) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization -- [sendAuthorizationSMS](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L127) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization +- [generateSmsText](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L116) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization +- [sendAuthorizationSms](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L127) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization - [createConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L139) (optional) - implementation is required in case the OAuth 2.0 consent step is enabled - [validateConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L153) (optional) - implementation is required in case the OAuth 2.0 consent step is enabled - [saveConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L165) (optional) - implementation is required in case the OAuth 2.0 consent step is enabled diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java index ef4a32e1..a7489964 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java @@ -113,7 +113,7 @@ public interface DataAdapter { * @return Generated SMS text with OTP authorization code. * @throws InvalidOperationContextException Thrown when operation context is invalid. */ - String generateSMSText(String userId, String organizationId, OperationContext operationContext, AuthorizationCode authorizationCode, String lang) throws InvalidOperationContextException; + String generateSmsText(String userId, String organizationId, OperationContext operationContext, AuthorizationCode authorizationCode, String lang) throws InvalidOperationContextException; /** * Send an authorization SMS with generated OTP. @@ -122,9 +122,9 @@ public interface DataAdapter { * @param messageText Text of SMS message. * @param operationContext Operation context. * @throws DataAdapterRemoteException Thrown when remote communication fails. - * @throws SMSAuthorizationFailedException Thrown when message could not be created. + * @throws SmsAuthorizationFailedException Thrown when message could not be created. */ - void sendAuthorizationSMS(String userId, String organizationId, String messageText, OperationContext operationContext) throws DataAdapterRemoteException, SMSAuthorizationFailedException; + void sendAuthorizationSms(String userId, String organizationId, String messageText, OperationContext operationContext) throws DataAdapterRemoteException, SmsAuthorizationFailedException; /** * Create OAuth 2.0 consent form - prepare HTML text of consent form and add form options. diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java similarity index 72% rename from powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java rename to powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java index 7a3073db..61aceddd 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java @@ -22,16 +22,16 @@ import io.getlime.security.powerauth.app.dataadapter.exception.AuthenticationFailedException; import io.getlime.security.powerauth.app.dataadapter.exception.DataAdapterRemoteException; import io.getlime.security.powerauth.app.dataadapter.exception.InvalidOperationContextException; -import io.getlime.security.powerauth.app.dataadapter.exception.SMSAuthorizationFailedException; -import io.getlime.security.powerauth.app.dataadapter.impl.validation.CreateSMSAuthorizationRequestValidator; -import io.getlime.security.powerauth.app.dataadapter.repository.model.entity.SMSAuthorizationEntity; -import io.getlime.security.powerauth.app.dataadapter.service.SMSPersistenceService; +import io.getlime.security.powerauth.app.dataadapter.exception.SmsAuthorizationFailedException; +import io.getlime.security.powerauth.app.dataadapter.impl.validation.CreateSmsAuthorizationRequestValidator; +import io.getlime.security.powerauth.app.dataadapter.repository.model.entity.SmsAuthorizationEntity; +import io.getlime.security.powerauth.app.dataadapter.service.SmsPersistenceService; import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.AuthenticationType; -import io.getlime.security.powerauth.lib.dataadapter.model.request.CreateSMSAuthorizationRequest; -import io.getlime.security.powerauth.lib.dataadapter.model.request.VerifySMSAndPasswordRequest; -import io.getlime.security.powerauth.lib.dataadapter.model.request.VerifySMSAuthorizationRequest; -import io.getlime.security.powerauth.lib.dataadapter.model.response.CreateSMSAuthorizationResponse; +import io.getlime.security.powerauth.lib.dataadapter.model.request.CreateSmsAuthorizationRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.request.VerifySmsAndPasswordRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.request.VerifySmsAuthorizationRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.response.CreateSmsAuthorizationResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -47,12 +47,12 @@ */ @RestController @RequestMapping("/api/auth/sms") -public class SMSAuthorizationController { +public class SmsAuthorizationController { - private static final Logger logger = LoggerFactory.getLogger(SMSAuthorizationController.class); + private static final Logger logger = LoggerFactory.getLogger(SmsAuthorizationController.class); - private final SMSPersistenceService smsPersistenceService; - private final CreateSMSAuthorizationRequestValidator requestValidator; + private final SmsPersistenceService smsPersistenceService; + private final CreateSmsAuthorizationRequestValidator requestValidator; private final DataAdapter dataAdapter; /** @@ -62,7 +62,7 @@ public class SMSAuthorizationController { * @param dataAdapter Data adapter. */ @Autowired - public SMSAuthorizationController(SMSPersistenceService smsPersistenceService, CreateSMSAuthorizationRequestValidator requestValidator, DataAdapter dataAdapter) { + public SmsAuthorizationController(SmsPersistenceService smsPersistenceService, CreateSmsAuthorizationRequestValidator requestValidator, DataAdapter dataAdapter) { this.smsPersistenceService = smsPersistenceService; this.requestValidator = requestValidator; this.dataAdapter = dataAdapter; @@ -83,15 +83,15 @@ private void initBinder(WebDataBinder binder) { * @param request Request data. * @return Response with message ID. * @throws DataAdapterRemoteException Thrown in case of remote communication errors. - * @throws SMSAuthorizationFailedException Thrown in case that SMS message could not be delivered. + * @throws SmsAuthorizationFailedException Thrown in case that SMS message could not be delivered. */ @RequestMapping(value = "create", method = RequestMethod.POST) - public ObjectResponse createAuthorizationSMS(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException, SMSAuthorizationFailedException, InvalidOperationContextException { - logger.info("Received createAuthorizationSMS request, operation ID: "+request.getRequestObject().getOperationContext().getId()); - CreateSMSAuthorizationRequest smsRequest = request.getRequestObject(); + public ObjectResponse createAuthorizationSms(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException, SmsAuthorizationFailedException, InvalidOperationContextException { + logger.info("Received createAuthorizationSms request, operation ID: "+request.getRequestObject().getOperationContext().getId()); + CreateSmsAuthorizationRequest smsRequest = request.getRequestObject(); // Create authorization SMS and persist it. - SMSAuthorizationEntity smsEntity = createAuthorizationSMS(smsRequest); + SmsAuthorizationEntity smsEntity = createAuthorizationSms(smsRequest); // Send SMS with generated text to target user. String userId = smsEntity.getUserId(); @@ -99,11 +99,11 @@ public ObjectResponse createAuthorizationSMS(@Va OperationContext operationContext = smsRequest.getOperationContext(); String messageId = smsEntity.getMessageId(); String messageText = smsEntity.getMessageText(); - dataAdapter.sendAuthorizationSMS(userId, organizationId, messageText, operationContext); + dataAdapter.sendAuthorizationSms(userId, organizationId, messageText, operationContext); // Create response. - CreateSMSAuthorizationResponse response = new CreateSMSAuthorizationResponse(messageId); - logger.info("The createAuthorizationSMS request succeeded, operation ID: "+request.getRequestObject().getOperationContext().getId()); + CreateSmsAuthorizationResponse response = new CreateSmsAuthorizationResponse(messageId); + logger.info("The createAuthorizationSms request succeeded, operation ID: "+request.getRequestObject().getOperationContext().getId()); return new ObjectResponse<>(response); } @@ -112,12 +112,12 @@ public ObjectResponse createAuthorizationSMS(@Va * @param smsRequest Create SMS request. * @return SMS entity. */ - private SMSAuthorizationEntity createAuthorizationSMS(@Valid CreateSMSAuthorizationRequest smsRequest) throws InvalidOperationContextException { + private SmsAuthorizationEntity createAuthorizationSms(@Valid CreateSmsAuthorizationRequest smsRequest) throws InvalidOperationContextException { String userId = smsRequest.getUserId(); String organizationId = smsRequest.getOrganizationId(); OperationContext operationContext = smsRequest.getOperationContext(); String lang = smsRequest.getLang(); - return smsPersistenceService.createAuthorizationSMS(userId, organizationId, operationContext, lang); + return smsPersistenceService.createAuthorizationSms(userId, organizationId, operationContext, lang); } /** @@ -125,17 +125,17 @@ private SMSAuthorizationEntity createAuthorizationSMS(@Valid CreateSMSAuthorizat * * @param request Request data. * @return Authorization response. - * @throws SMSAuthorizationFailedException Thrown in case that SMS verification fails. + * @throws SmsAuthorizationFailedException Thrown in case that SMS verification fails. */ @RequestMapping(value = "verify", method = RequestMethod.POST) - public Response verifyAuthorizationSMS(@RequestBody ObjectRequest request) throws SMSAuthorizationFailedException { - logger.info("Received verifyAuthorizationSMS request, operation ID: "+request.getRequestObject().getOperationContext().getId()); - VerifySMSAuthorizationRequest verifyRequest = request.getRequestObject(); + public Response verifyAuthorizationSms(@RequestBody ObjectRequest request) throws SmsAuthorizationFailedException { + logger.info("Received verifyAuthorizationSms request, operation ID: "+request.getRequestObject().getOperationContext().getId()); + VerifySmsAuthorizationRequest verifyRequest = request.getRequestObject(); String messageId = verifyRequest.getMessageId(); String authorizationCode = verifyRequest.getAuthorizationCode(); // Verify authorization code - smsPersistenceService.verifyAuthorizationSMS(messageId, authorizationCode); - logger.info("The verifyAuthorizationSMS request succeeded, operation ID: "+request.getRequestObject().getOperationContext().getId()); + smsPersistenceService.verifyAuthorizationSms(messageId, authorizationCode, false); + logger.info("The verifyAuthorizationSms request succeeded, operation ID: "+request.getRequestObject().getOperationContext().getId()); return new Response(); } @@ -144,18 +144,18 @@ public Response verifyAuthorizationSMS(@RequestBody ObjectRequest request) throws SMSAuthorizationFailedException, AuthenticationFailedException, DataAdapterRemoteException { - logger.info("Received verifyAuthorizationSMSAndPassword request, operation ID: "+request.getRequestObject().getOperationContext().getId()); - VerifySMSAndPasswordRequest verifyRequest = request.getRequestObject(); + public Response verifyAuthorizationSmsAndPassword(@RequestBody ObjectRequest request) throws SmsAuthorizationFailedException, AuthenticationFailedException, DataAdapterRemoteException { + logger.info("Received verifyAuthorizationSmsAndPassword request, operation ID: "+request.getRequestObject().getOperationContext().getId()); + VerifySmsAndPasswordRequest verifyRequest = request.getRequestObject(); // Verify authorization code String messageId = verifyRequest.getMessageId(); String authorizationCode = verifyRequest.getAuthorizationCode(); - smsPersistenceService.verifyAuthorizationSMS(messageId, authorizationCode); + smsPersistenceService.verifyAuthorizationSms(messageId, authorizationCode, true); // Verify user password String userId = verifyRequest.getUserId(); String password = verifyRequest.getPassword(); @@ -164,7 +164,7 @@ public Response verifyAuthorizationSMSAndPassword(@RequestBody ObjectRequest digestItems = new ArrayList<>(); switch (operationName) { - case "login": { + case "login": + case "login_2fa": { digestItems.add(operationName); break; } @@ -201,11 +202,12 @@ public AuthorizationCode generateAuthorizationCode(String userId, String organiz } @Override - public String generateSMSText(String userId, String organizationId, OperationContext operationContext, AuthorizationCode authorizationCode, String lang) throws InvalidOperationContextException { + public String generateSmsText(String userId, String organizationId, OperationContext operationContext, AuthorizationCode authorizationCode, String lang) throws InvalidOperationContextException { String operationName = operationContext.getName(); String[] messageArgs; switch (operationName) { - case "login": { + case "login": + case "login_2fa": { messageArgs = new String[]{authorizationCode.getCode()}; break; } @@ -226,14 +228,14 @@ public String generateSMSText(String userId, String organizationId, OperationCon } @Override - public void sendAuthorizationSMS(String userId, String organizationId, String messageText, OperationContext operationContext) throws DataAdapterRemoteException, SMSAuthorizationFailedException { + public void sendAuthorizationSms(String userId, String organizationId, String messageText, OperationContext operationContext) throws DataAdapterRemoteException, SmsAuthorizationFailedException { // Add here code to send the SMS OTP message to user identified by userId with messageText. - // In case message delivery fails, throw an SMSAuthorizationFailedException. + // In case message delivery fails, throw an SmsAuthorizationFailedException. } @Override public CreateConsentFormResponse createConsentForm(String userId, String organizationId, OperationContext operationContext, String lang) throws DataAdapterRemoteException, InvalidOperationContextException { - if ("login".equals(operationContext.getName())) { + if ("login".equals(operationContext.getName()) || "login_2fa".equals(operationContext.getName())) { // Create default consent CreateConsentFormResponse response = new CreateConsentFormResponse(); if ("cs".equals(lang)) { @@ -293,7 +295,7 @@ public ValidateConsentFormResponse validateConsentForm(String userId, String org if (options == null || options.isEmpty()) { throw new InvalidConsentDataException("Missing options for consent"); } - if ("login".equals(operationContext.getName())) { + if ("login".equals(operationContext.getName()) || "login_2fa".equals(operationContext.getName())) { if (options.size() != 1) { throw new InvalidConsentDataException("Unexpected options count for consent"); } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java index f26585a3..9181c552 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java @@ -76,7 +76,7 @@ public void validate(@Nullable Object o, @NonNull Errors errors) { } ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.organizationId", "login.organizationId.empty"); - if (username!=null && organizationId.length() > 256) { + if (organizationId!=null && organizationId.length() > 256) { errors.rejectValue("requestObject.organizationId", "login.organizationId.long"); } } else if (objectRequest.getRequestObject() instanceof AuthenticationRequest) { diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSMSAuthorizationRequestValidator.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSmsAuthorizationRequestValidator.java similarity index 94% rename from powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSMSAuthorizationRequestValidator.java rename to powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSmsAuthorizationRequestValidator.java index 2ae77ef1..b18bac28 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSMSAuthorizationRequestValidator.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSmsAuthorizationRequestValidator.java @@ -20,7 +20,7 @@ import io.getlime.security.powerauth.app.dataadapter.impl.service.OperationValueExtractionService; import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; import io.getlime.security.powerauth.lib.dataadapter.model.entity.attribute.AmountAttribute; -import io.getlime.security.powerauth.lib.dataadapter.model.request.CreateSMSAuthorizationRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.request.CreateSmsAuthorizationRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; @@ -39,7 +39,7 @@ * @author Roman Strobl, roman.strobl@wultra.com */ @Component -public class CreateSMSAuthorizationRequestValidator implements Validator { +public class CreateSmsAuthorizationRequestValidator implements Validator { private OperationValueExtractionService operationValueExtractionService; @@ -48,7 +48,7 @@ public class CreateSMSAuthorizationRequestValidator implements Validator { * @param operationValueExtractionService Operation form data service. */ @Autowired - public CreateSMSAuthorizationRequestValidator(OperationValueExtractionService operationValueExtractionService) { + public CreateSmsAuthorizationRequestValidator(OperationValueExtractionService operationValueExtractionService) { this.operationValueExtractionService = operationValueExtractionService; } @@ -70,12 +70,12 @@ public boolean supports(@NonNull Class clazz) { @Override @SuppressWarnings("unchecked") public void validate(@Nullable Object o, @NonNull Errors errors) { - ObjectRequest requestObject = (ObjectRequest) o; + ObjectRequest requestObject = (ObjectRequest) o; if (requestObject == null) { errors.rejectValue("requestObject.operationContext", "operationContext.missing"); return; } - CreateSMSAuthorizationRequest authRequest = requestObject.getRequestObject(); + CreateSmsAuthorizationRequest authRequest = requestObject.getRequestObject(); // update validation logic based on the real Data Adapter requirements String userId = authRequest.getUserId(); @@ -105,6 +105,7 @@ public void validate(@Nullable Object o, @NonNull Errors errors) { if (operationName != null) { switch (operationName) { case "login": + case "login_2fa": // no field validation required break; case "authorize_payment": diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/SMSAuthorizationRepository.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/SmsAuthorizationRepository.java similarity index 87% rename from powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/SMSAuthorizationRepository.java rename to powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/SmsAuthorizationRepository.java index 13110e6b..e9ba5076 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/SMSAuthorizationRepository.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/SmsAuthorizationRepository.java @@ -15,7 +15,7 @@ */ package io.getlime.security.powerauth.app.dataadapter.repository; -import io.getlime.security.powerauth.app.dataadapter.repository.model.entity.SMSAuthorizationEntity; +import io.getlime.security.powerauth.app.dataadapter.repository.model.entity.SmsAuthorizationEntity; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; @@ -26,6 +26,6 @@ * @author Roman Strobl, roman.strobl@wultra.com */ @Component -public interface SMSAuthorizationRepository extends CrudRepository { +public interface SmsAuthorizationRepository extends CrudRepository { } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/model/entity/SMSAuthorizationEntity.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/model/entity/SmsAuthorizationEntity.java similarity index 98% rename from powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/model/entity/SMSAuthorizationEntity.java rename to powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/model/entity/SmsAuthorizationEntity.java index 1dfda24a..c40dea6f 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/model/entity/SMSAuthorizationEntity.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/model/entity/SmsAuthorizationEntity.java @@ -29,7 +29,7 @@ */ @Entity @Table(name = "da_sms_authorization") -public class SMSAuthorizationEntity implements Serializable { +public class SmsAuthorizationEntity implements Serializable { private static final long serialVersionUID = 6432269422572862762L; @@ -295,7 +295,7 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - SMSAuthorizationEntity that = (SMSAuthorizationEntity) o; + SmsAuthorizationEntity that = (SmsAuthorizationEntity) o; return messageId.equals(that.messageId); } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SMSPersistenceService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SmsPersistenceService.java similarity index 76% rename from powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SMSPersistenceService.java rename to powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SmsPersistenceService.java index baeb6c1c..b1cc4ca4 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SMSPersistenceService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SmsPersistenceService.java @@ -17,10 +17,10 @@ import io.getlime.security.powerauth.app.dataadapter.configuration.DataAdapterConfiguration; import io.getlime.security.powerauth.app.dataadapter.exception.InvalidOperationContextException; -import io.getlime.security.powerauth.app.dataadapter.exception.SMSAuthorizationFailedException; +import io.getlime.security.powerauth.app.dataadapter.exception.SmsAuthorizationFailedException; import io.getlime.security.powerauth.app.dataadapter.impl.service.DataAdapterService; -import io.getlime.security.powerauth.app.dataadapter.repository.SMSAuthorizationRepository; -import io.getlime.security.powerauth.app.dataadapter.repository.model.entity.SMSAuthorizationEntity; +import io.getlime.security.powerauth.app.dataadapter.repository.SmsAuthorizationRepository; +import io.getlime.security.powerauth.app.dataadapter.repository.model.entity.SmsAuthorizationEntity; import io.getlime.security.powerauth.lib.dataadapter.model.entity.AuthorizationCode; import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; import org.joda.time.DateTime; @@ -37,10 +37,10 @@ * @author Roman Strobl, roman.strobl@wultra.com */ @Service -public class SMSPersistenceService { +public class SmsPersistenceService { private final DataAdapterService dataAdapterService; - private final SMSAuthorizationRepository smsAuthorizationRepository; + private final SmsAuthorizationRepository smsAuthorizationRepository; private final DataAdapterConfiguration dataAdapterConfiguration; /** @@ -50,7 +50,7 @@ public class SMSPersistenceService { * @param dataAdapterConfiguration Data adapter configuration. */ @Autowired - public SMSPersistenceService(DataAdapterService dataAdapterService, SMSAuthorizationRepository smsAuthorizationRepository, DataAdapterConfiguration dataAdapterConfiguration) { + public SmsPersistenceService(DataAdapterService dataAdapterService, SmsAuthorizationRepository smsAuthorizationRepository, DataAdapterConfiguration dataAdapterConfiguration) { this.dataAdapterService = dataAdapterService; this.smsAuthorizationRepository = smsAuthorizationRepository; this.dataAdapterConfiguration = dataAdapterConfiguration; @@ -64,7 +64,7 @@ public SMSPersistenceService(DataAdapterService dataAdapterService, SMSAuthoriza * @param lang Language for message text. * @return Created entity with SMS message details. */ - public SMSAuthorizationEntity createAuthorizationSMS(String userId, String organizationId, OperationContext operationContext, String lang) throws InvalidOperationContextException { + public SmsAuthorizationEntity createAuthorizationSms(String userId, String organizationId, OperationContext operationContext, String lang) throws InvalidOperationContextException { String operationId = operationContext.getId(); String operationName = operationContext.getName(); @@ -75,9 +75,9 @@ public SMSAuthorizationEntity createAuthorizationSMS(String userId, String organ AuthorizationCode authorizationCode = dataAdapterService.generateAuthorizationCode(userId, organizationId, operationContext); // generate message text, include previously generated authorization code - String messageText = dataAdapterService.generateSMSText(userId, organizationId, operationContext, authorizationCode, lang); + String messageText = dataAdapterService.generateSmsText(userId, organizationId, operationContext, authorizationCode, lang); - SMSAuthorizationEntity smsEntity = new SMSAuthorizationEntity(); + SmsAuthorizationEntity smsEntity = new SmsAuthorizationEntity(); smsEntity.setMessageId(messageId); smsEntity.setOperationId(operationId); smsEntity.setUserId(userId); @@ -102,14 +102,15 @@ public SMSAuthorizationEntity createAuthorizationSMS(String userId, String organ * Verify an OTP authorization code. * @param messageId Message ID. * @param authorizationCode Authorization code. - * @throws SMSAuthorizationFailedException Thrown when SMS authorization fails. + * @param smsAndPasswordCombined Whether SMS code is used together with password. + * @throws SmsAuthorizationFailedException Thrown when SMS authorization fails. */ - public void verifyAuthorizationSMS(String messageId, String authorizationCode) throws SMSAuthorizationFailedException { - Optional smsEntityOptional = smsAuthorizationRepository.findById(messageId); + public void verifyAuthorizationSms(String messageId, String authorizationCode, boolean smsAndPasswordCombined) throws SmsAuthorizationFailedException { + Optional smsEntityOptional = smsAuthorizationRepository.findById(messageId); if (!smsEntityOptional.isPresent()) { - throw new SMSAuthorizationFailedException("smsAuthorization.invalidMessage"); + throw new SmsAuthorizationFailedException("smsAuthorization.invalidMessage"); } - SMSAuthorizationEntity smsEntity = smsEntityOptional.get(); + SmsAuthorizationEntity smsEntity = smsEntityOptional.get(); // increase number of verification tries and save entity smsEntity.setVerifyRequestCount(smsEntity.getVerifyRequestCount() + 1); smsAuthorizationRepository.save(smsEntity); @@ -117,22 +118,28 @@ public void verifyAuthorizationSMS(String messageId, String authorizationCode) t final Integer remainingAttempts = dataAdapterConfiguration.getSmsOtpMaxVerifyTriesPerMessage() - smsEntity.getVerifyRequestCount(); if (smsEntity.getAuthorizationCode() == null || smsEntity.getAuthorizationCode().isEmpty()) { - SMSAuthorizationFailedException ex = new SMSAuthorizationFailedException("smsAuthorization.invalidCode"); + SmsAuthorizationFailedException ex = new SmsAuthorizationFailedException("smsAuthorization.invalidCode"); ex.setRemainingAttempts(remainingAttempts); throw ex; } if (smsEntity.isExpired()) { - throw new SMSAuthorizationFailedException("smsAuthorization.expired"); + throw new SmsAuthorizationFailedException("smsAuthorization.expired"); } - if (smsEntity.isVerified()) { - throw new SMSAuthorizationFailedException("smsAuthorization.alreadyVerified"); + if (!smsAndPasswordCombined && smsEntity.isVerified()) { + throw new SmsAuthorizationFailedException("smsAuthorization.alreadyVerified"); } if (smsEntity.getVerifyRequestCount() > dataAdapterConfiguration.getSmsOtpMaxVerifyTriesPerMessage()) { - throw new SMSAuthorizationFailedException("smsAuthorization.maxAttemptsExceeded"); + throw new SmsAuthorizationFailedException("smsAuthorization.maxAttemptsExceeded"); } String authorizationCodeExpected = smsEntity.getAuthorizationCode(); if (!authorizationCode.equals(authorizationCodeExpected)) { - SMSAuthorizationFailedException ex = new SMSAuthorizationFailedException("smsAuthorization.failed"); + if (smsAndPasswordCombined) { + // Use authentication error so that attacker cannot determine whether password or SMS code was invalid + SmsAuthorizationFailedException ex = new SmsAuthorizationFailedException("login.authenticationFailed"); + ex.setRemainingAttempts(remainingAttempts); + throw ex; + } + SmsAuthorizationFailedException ex = new SmsAuthorizationFailedException("smsAuthorization.failed"); ex.setRemainingAttempts(remainingAttempts); throw ex; } diff --git a/powerauth-data-adapter/src/main/resources/static/resources/messages_cs.properties b/powerauth-data-adapter/src/main/resources/static/resources/messages_cs.properties index ed0e5624..d6e46078 100644 --- a/powerauth-data-adapter/src/main/resources/static/resources/messages_cs.properties +++ b/powerauth-data-adapter/src/main/resources/static/resources/messages_cs.properties @@ -1,3 +1,4 @@ login.smsText=Autorizaฤnรญ kรณd pro pล™ihlรกลกenรญ je {0}. +login_2fa.smsText=Autorizaฤnรญ kรณd pro pล™ihlรกลกenรญ je {0}. authorize_payment.smsText=Autorizaฤnรญ kรณd pro platbu {0} {1} na รบฤet {2} je {3}. operationReview.balanceTooLow=Nรญzkรฝ zลฏstatek na รบฤtu diff --git a/powerauth-data-adapter/src/main/resources/static/resources/messages_en.properties b/powerauth-data-adapter/src/main/resources/static/resources/messages_en.properties index 9f218e85..a2aab7c9 100644 --- a/powerauth-data-adapter/src/main/resources/static/resources/messages_en.properties +++ b/powerauth-data-adapter/src/main/resources/static/resources/messages_en.properties @@ -1,3 +1,4 @@ login.smsText=Authorization code for login is {0}. +login_2fa.smsText=Authorization code for login is {0}. authorize_payment.smsText=Authorization code for payment of {0} {1} to account {2} is {3}. operationReview.balanceTooLow=Low account balance From de98cbb0e54c9afe5556b0d52527b7a80132bf7f Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Tue, 9 Jul 2019 15:40:54 +0200 Subject: [PATCH 31/79] Enable SCA operation names in Data Adapter --- .../impl/service/DataAdapterService.java | 20 ++++++++++--------- ...reateSmsAuthorizationRequestValidator.java | 3 ++- .../static/resources/messages_cs.properties | 3 ++- .../static/resources/messages_en.properties | 3 ++- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index 302f05a7..e1e01c65 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -92,7 +92,7 @@ public DecorateOperationFormDataResponse decorateFormData(String userId, String // Replace mock bank account data with real data loaded from the bank backend. // In case the bank account selection is disabled, return an empty list. - if (!"authorize_payment".equals(operationName)) { + if (!"authorize_payment".equals(operationName) && !"authorize_payment_sca".equals(operationName)) { // return empty list for operations other than authorize_payment return new DecorateOperationFormDataResponse(formData); } @@ -175,11 +175,12 @@ public AuthorizationCode generateAuthorizationCode(String userId, String organiz List digestItems = new ArrayList<>(); switch (operationName) { case "login": - case "login_2fa": { + case "login_sca": { digestItems.add(operationName); break; } - case "authorize_payment": { + case "authorize_payment": + case "authorize_payment_sca": { AmountAttribute amountAttribute = operationValueExtractionService.getAmount(operationContext); String account = operationValueExtractionService.getAccount(operationContext); BigDecimal amount = amountAttribute.getAmount(); @@ -207,11 +208,12 @@ public String generateSmsText(String userId, String organizationId, OperationCon String[] messageArgs; switch (operationName) { case "login": - case "login_2fa": { + case "login_sca": { messageArgs = new String[]{authorizationCode.getCode()}; break; } - case "authorize_payment": { + case "authorize_payment": + case "authorize_payment_sca": { AmountAttribute amountAttribute = operationValueExtractionService.getAmount(operationContext); String account = operationValueExtractionService.getAccount(operationContext); BigDecimal amount = amountAttribute.getAmount(); @@ -235,7 +237,7 @@ public void sendAuthorizationSms(String userId, String organizationId, String me @Override public CreateConsentFormResponse createConsentForm(String userId, String organizationId, OperationContext operationContext, String lang) throws DataAdapterRemoteException, InvalidOperationContextException { - if ("login".equals(operationContext.getName()) || "login_2fa".equals(operationContext.getName())) { + if ("login".equals(operationContext.getName()) || "login_sca".equals(operationContext.getName())) { // Create default consent CreateConsentFormResponse response = new CreateConsentFormResponse(); if ("cs".equals(lang)) { @@ -256,7 +258,7 @@ public CreateConsentFormResponse createConsentForm(String userId, String organiz response.getOptions().add(option1); return response; } - if ("authorize_payment".equals(operationContext.getName())) { + if ("authorize_payment".equals(operationContext.getName()) || "authorize_payment_sca".equals(operationContext.getName())) { CreateConsentFormResponse response = new CreateConsentFormResponse(); if ("cs".equals(lang)) { response.setConsentHtml("Tรญmto potvrzuji, ลพe jsem inicioval tuto platebnรญ operaci a souhlasรญm s jejรญm dokonฤenรญm."); @@ -295,7 +297,7 @@ public ValidateConsentFormResponse validateConsentForm(String userId, String org if (options == null || options.isEmpty()) { throw new InvalidConsentDataException("Missing options for consent"); } - if ("login".equals(operationContext.getName()) || "login_2fa".equals(operationContext.getName())) { + if ("login".equals(operationContext.getName()) || "login_sca".equals(operationContext.getName())) { if (options.size() != 1) { throw new InvalidConsentDataException("Unexpected options count for consent"); } @@ -326,7 +328,7 @@ public ValidateConsentFormResponse validateConsentForm(String userId, String org } return response; } - if ("authorize_payment".equals(operationContext.getName())) { + if ("authorize_payment".equals(operationContext.getName()) || "authorize_payment_sca".equals(operationContext.getName())) { if (options.size() != 2) { throw new InvalidConsentDataException("Unexpected options count for consent"); } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSmsAuthorizationRequestValidator.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSmsAuthorizationRequestValidator.java index b18bac28..eb68f408 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSmsAuthorizationRequestValidator.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSmsAuthorizationRequestValidator.java @@ -105,10 +105,11 @@ public void validate(@Nullable Object o, @NonNull Errors errors) { if (operationName != null) { switch (operationName) { case "login": - case "login_2fa": + case "login_sca": // no field validation required break; case "authorize_payment": + case "authorize_payment_sca": AmountAttribute amountAttribute; try { amountAttribute = operationValueExtractionService.getAmount(authRequest.getOperationContext()); diff --git a/powerauth-data-adapter/src/main/resources/static/resources/messages_cs.properties b/powerauth-data-adapter/src/main/resources/static/resources/messages_cs.properties index d6e46078..cde9b6bc 100644 --- a/powerauth-data-adapter/src/main/resources/static/resources/messages_cs.properties +++ b/powerauth-data-adapter/src/main/resources/static/resources/messages_cs.properties @@ -1,4 +1,5 @@ login.smsText=Autorizaฤnรญ kรณd pro pล™ihlรกลกenรญ je {0}. -login_2fa.smsText=Autorizaฤnรญ kรณd pro pล™ihlรกลกenรญ je {0}. +login_sca.smsText=Autorizaฤnรญ kรณd pro pล™ihlรกลกenรญ je {0}. authorize_payment.smsText=Autorizaฤnรญ kรณd pro platbu {0} {1} na รบฤet {2} je {3}. +authorize_payment_sca.smsText=Autorizaฤnรญ kรณd pro platbu {0} {1} na รบฤet {2} je {3}. operationReview.balanceTooLow=Nรญzkรฝ zลฏstatek na รบฤtu diff --git a/powerauth-data-adapter/src/main/resources/static/resources/messages_en.properties b/powerauth-data-adapter/src/main/resources/static/resources/messages_en.properties index a2aab7c9..06fcc83d 100644 --- a/powerauth-data-adapter/src/main/resources/static/resources/messages_en.properties +++ b/powerauth-data-adapter/src/main/resources/static/resources/messages_en.properties @@ -1,4 +1,5 @@ login.smsText=Authorization code for login is {0}. -login_2fa.smsText=Authorization code for login is {0}. +login_sca.smsText=Authorization code for login is {0}. authorize_payment.smsText=Authorization code for payment of {0} {1} to account {2} is {3}. +authorize_payment_sca.smsText=Authorization code for payment of {0} {1} to account {2} is {3}. operationReview.balanceTooLow=Low account balance From 48539eaa9ff5fa5fb30ed05660699778e5e8b759 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Wed, 10 Jul 2019 09:46:55 +0200 Subject: [PATCH 32/79] Send correct form data in APPROVAL_SCA --- .../app/dataadapter/impl/service/DataAdapterService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index e1e01c65..9078d247 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -92,8 +92,8 @@ public DecorateOperationFormDataResponse decorateFormData(String userId, String // Replace mock bank account data with real data loaded from the bank backend. // In case the bank account selection is disabled, return an empty list. - if (!"authorize_payment".equals(operationName) && !"authorize_payment_sca".equals(operationName)) { - // return empty list for operations other than authorize_payment + if ((!"authorize_payment".equals(operationName) && !"authorize_payment_sca".equals(operationName))) { + // return empty list for operations other than authorize_payment or authorize_payment_sca return new DecorateOperationFormDataResponse(formData); } From 4ae734302981cd0b21189157fab41987c5151911 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Wed, 10 Jul 2019 13:31:06 +0200 Subject: [PATCH 33/79] Fix formatting --- .../impl/validation/AuthenticationRequestValidator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java index 9181c552..445279ab 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java @@ -71,12 +71,12 @@ public void validate(@Nullable Object o, @NonNull Errors errors) { errors.rejectValue("requestObject.operationContext", "operationContext.missing"); } ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.username", "login.username.empty"); - if (username!=null && username.length() > 30) { + if (username != null && username.length() > 30) { errors.rejectValue("requestObject.username", "login.username.long"); } ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.organizationId", "login.organizationId.empty"); - if (organizationId!=null && organizationId.length() > 256) { + if (organizationId != null && organizationId.length() > 256) { errors.rejectValue("requestObject.organizationId", "login.organizationId.long"); } } else if (objectRequest.getRequestObject() instanceof AuthenticationRequest) { From 3611851dae923e36cec4cc954c914f2890bb60b0 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Fri, 12 Jul 2019 19:36:16 +0200 Subject: [PATCH 34/79] Fix #90: Updates for strict SCA compliance --- .../app/dataadapter/api/DataAdapter.java | 69 +++++++++-- .../controller/AuthenticationController.java | 20 ++-- .../controller/ConsentController.java | 22 ++++ .../SmsAuthorizationController.java | 84 +++++-------- .../AuthenticationFailedException.java | 74 ------------ .../exception/DefaultExceptionResolver.java | 29 ----- .../SmsAuthorizationFailedException.java | 77 ------------ .../impl/service/DataAdapterService.java | 113 +++++++++++++++--- .../AuthenticationRequestValidator.java | 18 +-- .../ConsentFormRequestValidator.java | 11 +- .../service/SmsPersistenceService.java | 82 ++++++------- 11 files changed, 277 insertions(+), 322 deletions(-) delete mode 100644 powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/AuthenticationFailedException.java delete mode 100644 powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/SmsAuthorizationFailedException.java diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java index a7489964..53977835 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java @@ -17,7 +17,6 @@ import io.getlime.security.powerauth.app.dataadapter.exception.*; import io.getlime.security.powerauth.lib.dataadapter.model.entity.*; -import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.AuthenticationType; import io.getlime.security.powerauth.lib.dataadapter.model.response.*; import java.util.List; @@ -34,6 +33,8 @@ public interface DataAdapter { * @param username Username which user uses for authentication. * @param organizationId Organization ID for this request. * @param operationContext Operation context. + * @throws DataAdapterRemoteException Thrown when remote communication fails. + * @throws UserNotFoundException Thrown when user does not exist. */ UserDetailResponse lookupUser(String username, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, UserNotFoundException; @@ -41,15 +42,13 @@ public interface DataAdapter { * Authenticate user using provided credentials. * @param userId User ID for user authentication. * @param password Password for user authentication. - * @param authenticationType Authentication type specifying optional password encryption. - * @param cipherTransformation Cipher transformation used for encryption in case password is encrypted. + * @param authenticationContext Authentication context. * @param organizationId Organization ID. * @param operationContext Operation context. - * @return UserDetailResponse Response with user details. + * @return User authentication result. * @throws DataAdapterRemoteException Thrown when remote communication fails. - * @throws AuthenticationFailedException Thrown when authentication fails. */ - UserDetailResponse authenticateUser(String userId, String password, AuthenticationType authenticationType, String cipherTransformation, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, AuthenticationFailedException; + UserAuthenticationResponse authenticateUser(String userId, String password, AuthenticationContext authenticationContext, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException; /** * Fetch user detail for given user. @@ -93,6 +92,17 @@ public interface DataAdapter { */ void operationChangedNotification(String userId, String organizationId, OperationChange operationChange, OperationContext operationContext) throws DataAdapterRemoteException; + /** + * Create authorization SMS. + * @param userId User ID. + * @param organizationId Organization ID. + * @param operationContext Operation context. + * @param lang Language for localization. + * @throws InvalidOperationContextException Thrown when operation context is invalid. + * @throws DataAdapterRemoteException Thrown when remote communication fails or SMS message could not be delivered. + */ + String createAuthorizationSms(String userId, String organizationId, OperationContext operationContext, String lang) throws InvalidOperationContextException, DataAdapterRemoteException; + /** * Generate authorization code for SMS authorization. * @param userId User ID. @@ -110,21 +120,60 @@ public interface DataAdapter { * @param operationContext Operation context. * @param authorizationCode Authorization code. * @param lang Language for localization. - * @return Generated SMS text with OTP authorization code. + * @return Generated SMS text with authorization code. * @throws InvalidOperationContextException Thrown when operation context is invalid. */ String generateSmsText(String userId, String organizationId, OperationContext operationContext, AuthorizationCode authorizationCode, String lang) throws InvalidOperationContextException; /** - * Send an authorization SMS with generated OTP. + * Send an authorization SMS with generated authorization code. * @param userId User ID. * @param organizationId Organization ID. + * @param messageId Message ID. * @param messageText Text of SMS message. * @param operationContext Operation context. + * @throws DataAdapterRemoteException Thrown when remote communication fails or SMS message could not be delivered. + */ + void sendAuthorizationSms(String userId, String organizationId, String messageId, String messageText, OperationContext operationContext) throws DataAdapterRemoteException; + + /** + * Verify authorization code from SMS message. + * @param userId User ID. + * @param organizationId Organization ID. + * @param messageId Message ID. + * @param authorizationCode Authorization code. + * @param operationContext Operation context. + * @return SMS authorization code verification response. * @throws DataAdapterRemoteException Thrown when remote communication fails. - * @throws SmsAuthorizationFailedException Thrown when message could not be created. + * @throws InvalidOperationContextException Thrown when operation context is invalid. + */ + VerifySmsAuthorizationResponse verifyAuthorizationSms(String userId, String organizationId, String messageId, String authorizationCode, OperationContext operationContext) throws DataAdapterRemoteException, InvalidOperationContextException; + + /** + * Verify authorization code from SMS message together with user password. + * @param userId User ID. + * @param organizationId Organization ID. + * @param messageId Message ID. + * @param authorizationCode Authorization code. + * @param operationContext Operation context. + * @param authenticationContext Authentication context. + * @param password User password. + * @return SMS authorization code and password verification response. + * @throws DataAdapterRemoteException Thrown when remote communication fails. + * @throws InvalidOperationContextException Thrown when operation context is invalid. + */ + VerifySmsAndPasswordResponse verifyAuthorizationSmsAndPassword(String userId, String organizationId, String messageId, String authorizationCode, OperationContext operationContext, AuthenticationContext authenticationContext, String password) throws DataAdapterRemoteException, InvalidOperationContextException; + + /** + * Decide whether OAuth 2.0 consent form should be displayed based on operation context. + * @param userId User ID. + * @param organizationId Organization ID. + * @param operationContext Operation context. + * @return Response with information whether consent form should be displayed. + * @throws DataAdapterRemoteException Thrown when remote communication fails. + * @throws InvalidOperationContextException Thrown when operation context is invalid. */ - void sendAuthorizationSms(String userId, String organizationId, String messageText, OperationContext operationContext) throws DataAdapterRemoteException, SmsAuthorizationFailedException; + InitConsentFormResponse initConsentForm(String userId, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, InvalidOperationContextException; /** * Create OAuth 2.0 consent form - prepare HTML text of consent form and add form options. diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java index dd339427..e09123e9 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java @@ -18,16 +18,15 @@ import io.getlime.core.rest.model.base.request.ObjectRequest; import io.getlime.core.rest.model.base.response.ObjectResponse; import io.getlime.security.powerauth.app.dataadapter.api.DataAdapter; -import io.getlime.security.powerauth.app.dataadapter.exception.AuthenticationFailedException; import io.getlime.security.powerauth.app.dataadapter.exception.DataAdapterRemoteException; import io.getlime.security.powerauth.app.dataadapter.exception.UserNotFoundException; import io.getlime.security.powerauth.app.dataadapter.impl.validation.AuthenticationRequestValidator; +import io.getlime.security.powerauth.lib.dataadapter.model.entity.AuthenticationContext; import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; -import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.AuthenticationType; -import io.getlime.security.powerauth.lib.dataadapter.model.request.AuthenticationRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.request.UserAuthenticationRequest; import io.getlime.security.powerauth.lib.dataadapter.model.request.UserDetailRequest; import io.getlime.security.powerauth.lib.dataadapter.model.request.UserLookupRequest; -import io.getlime.security.powerauth.lib.dataadapter.model.response.AuthenticationResponse; +import io.getlime.security.powerauth.lib.dataadapter.model.response.UserAuthenticationResponse; import io.getlime.security.powerauth.lib.dataadapter.model.response.UserDetailResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -97,21 +96,18 @@ public ObjectResponse lookupUser(@Valid @RequestBody ObjectR * @param request Authenticate user request. * @return Response with authenticated user ID. * @throws DataAdapterRemoteException Thrown in case of remote communication errors. - * @throws AuthenticationFailedException Thrown in case that authentication fails. */ @RequestMapping(value = "/authenticate", method = RequestMethod.POST) - public ObjectResponse authenticate(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException, AuthenticationFailedException { + public ObjectResponse authenticate(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException { logger.info("Received authenticate request, user ID: {}, organization ID: {}, operation ID: {}", request.getRequestObject().getUserId(), request.getRequestObject().getOrganizationId(), request.getRequestObject().getOperationContext().getId()); - AuthenticationRequest authenticationRequest = request.getRequestObject(); + UserAuthenticationRequest authenticationRequest = request.getRequestObject(); String userId = authenticationRequest.getUserId(); String password = authenticationRequest.getPassword(); - AuthenticationType authenticationType = authenticationRequest.getAuthenticationType(); - String cipherTransformation = authenticationRequest.getCipherTransformation(); + AuthenticationContext authenticationContext = authenticationRequest.getAuthenticationContext(); String organizationId = authenticationRequest.getOrganizationId(); OperationContext operationContext = authenticationRequest.getOperationContext(); - UserDetailResponse userDetailResponse = dataAdapter.authenticateUser(userId, password, authenticationType, cipherTransformation, organizationId, operationContext); - AuthenticationResponse response = new AuthenticationResponse(userDetailResponse.getId(), userDetailResponse.getOrganizationId()); - logger.info("The authenticate request succeeded, user ID: {}, organization ID: {}, operation ID: {}", userDetailResponse.getId(), userDetailResponse.getOrganizationId(), request.getRequestObject().getOperationContext().getId()); + UserAuthenticationResponse response = dataAdapter.authenticateUser(userId, password, authenticationContext, organizationId, operationContext); + logger.info("The authenticate request succeeded, user ID: {}, organization ID: {}, operation ID: {}", userId, organizationId, request.getRequestObject().getOperationContext().getId()); return new ObjectResponse<>(response); } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ConsentController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ConsentController.java index abc64f24..1752138a 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ConsentController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ConsentController.java @@ -26,9 +26,11 @@ import io.getlime.security.powerauth.lib.dataadapter.model.entity.ConsentOption; import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; import io.getlime.security.powerauth.lib.dataadapter.model.request.CreateConsentFormRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.request.InitConsentFormRequest; import io.getlime.security.powerauth.lib.dataadapter.model.request.SaveConsentFormRequest; import io.getlime.security.powerauth.lib.dataadapter.model.request.ValidateConsentFormRequest; import io.getlime.security.powerauth.lib.dataadapter.model.response.CreateConsentFormResponse; +import io.getlime.security.powerauth.lib.dataadapter.model.response.InitConsentFormResponse; import io.getlime.security.powerauth.lib.dataadapter.model.response.SaveConsentFormResponse; import io.getlime.security.powerauth.lib.dataadapter.model.response.ValidateConsentFormResponse; import org.slf4j.Logger; @@ -74,6 +76,26 @@ private void initBinder(WebDataBinder binder) { binder.setValidator(requestValidator); } + /** + * Initialize OAuth 2.0 consent form - verify that consent form is required. + * @param request Initialize consent form request. + * @return Initialize consent form response. + * @throws DataAdapterRemoteException In case communication with remote system fails. + * @throws InvalidOperationContextException In case operation context is invalid. + */ + @RequestMapping(value = "/init", method = RequestMethod.POST) + public ObjectResponse initConsentForm(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException, InvalidOperationContextException { + logger.info("Received initConsentForm request for user: {}, operation ID: {}", + request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); + InitConsentFormRequest createRequest = request.getRequestObject(); + String userId = createRequest.getUserId(); + String organizationId = createRequest.getOrganizationId(); + OperationContext operationContext = createRequest.getOperationContext(); + InitConsentFormResponse response = dataAdapter.initConsentForm(userId, organizationId, operationContext); + logger.debug("The initConsentForm request succeeded"); + return new ObjectResponse<>(response); + } + /** * Create OAuth 2.0 consent form. * @param request Create consent form request. diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java index 61aceddd..d6625be9 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java @@ -17,21 +17,19 @@ import io.getlime.core.rest.model.base.request.ObjectRequest; import io.getlime.core.rest.model.base.response.ObjectResponse; -import io.getlime.core.rest.model.base.response.Response; import io.getlime.security.powerauth.app.dataadapter.api.DataAdapter; -import io.getlime.security.powerauth.app.dataadapter.exception.AuthenticationFailedException; import io.getlime.security.powerauth.app.dataadapter.exception.DataAdapterRemoteException; import io.getlime.security.powerauth.app.dataadapter.exception.InvalidOperationContextException; -import io.getlime.security.powerauth.app.dataadapter.exception.SmsAuthorizationFailedException; import io.getlime.security.powerauth.app.dataadapter.impl.validation.CreateSmsAuthorizationRequestValidator; -import io.getlime.security.powerauth.app.dataadapter.repository.model.entity.SmsAuthorizationEntity; import io.getlime.security.powerauth.app.dataadapter.service.SmsPersistenceService; +import io.getlime.security.powerauth.lib.dataadapter.model.entity.AuthenticationContext; import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; -import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.AuthenticationType; import io.getlime.security.powerauth.lib.dataadapter.model.request.CreateSmsAuthorizationRequest; import io.getlime.security.powerauth.lib.dataadapter.model.request.VerifySmsAndPasswordRequest; import io.getlime.security.powerauth.lib.dataadapter.model.request.VerifySmsAuthorizationRequest; import io.getlime.security.powerauth.lib.dataadapter.model.response.CreateSmsAuthorizationResponse; +import io.getlime.security.powerauth.lib.dataadapter.model.response.VerifySmsAndPasswordResponse; +import io.getlime.security.powerauth.lib.dataadapter.model.response.VerifySmsAuthorizationResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -83,60 +81,47 @@ private void initBinder(WebDataBinder binder) { * @param request Request data. * @return Response with message ID. * @throws DataAdapterRemoteException Thrown in case of remote communication errors. - * @throws SmsAuthorizationFailedException Thrown in case that SMS message could not be delivered. + * @throws InvalidOperationContextException Thrown in case operation context is invalid. */ @RequestMapping(value = "create", method = RequestMethod.POST) - public ObjectResponse createAuthorizationSms(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException, SmsAuthorizationFailedException, InvalidOperationContextException { - logger.info("Received createAuthorizationSms request, operation ID: "+request.getRequestObject().getOperationContext().getId()); + public ObjectResponse createAuthorizationSms(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException, InvalidOperationContextException { + logger.info("Received createAuthorizationSms request, operation ID: {}", request.getRequestObject().getOperationContext().getId()); CreateSmsAuthorizationRequest smsRequest = request.getRequestObject(); // Create authorization SMS and persist it. - SmsAuthorizationEntity smsEntity = createAuthorizationSms(smsRequest); - - // Send SMS with generated text to target user. - String userId = smsEntity.getUserId(); - String organizationId = smsEntity.getOrganizationId(); + String userId = smsRequest.getUserId(); + String organizationId = smsRequest.getOrganizationId(); OperationContext operationContext = smsRequest.getOperationContext(); - String messageId = smsEntity.getMessageId(); - String messageText = smsEntity.getMessageText(); - dataAdapter.sendAuthorizationSms(userId, organizationId, messageText, operationContext); + String lang = smsRequest.getLang(); + String messageId = dataAdapter.createAuthorizationSms(userId, organizationId, operationContext, lang); // Create response. CreateSmsAuthorizationResponse response = new CreateSmsAuthorizationResponse(messageId); - logger.info("The createAuthorizationSms request succeeded, operation ID: "+request.getRequestObject().getOperationContext().getId()); + logger.info("The createAuthorizationSms request succeeded, operation ID: {}", request.getRequestObject().getOperationContext().getId()); return new ObjectResponse<>(response); } /** - * Validates the request and sends SMS. - * @param smsRequest Create SMS request. - * @return SMS entity. - */ - private SmsAuthorizationEntity createAuthorizationSms(@Valid CreateSmsAuthorizationRequest smsRequest) throws InvalidOperationContextException { - String userId = smsRequest.getUserId(); - String organizationId = smsRequest.getOrganizationId(); - OperationContext operationContext = smsRequest.getOperationContext(); - String lang = smsRequest.getLang(); - return smsPersistenceService.createAuthorizationSms(userId, organizationId, operationContext, lang); - } - - /** - * Verify a SMS OTP authorization code. + * Verify authorization code from SMS message. * * @param request Request data. * @return Authorization response. - * @throws SmsAuthorizationFailedException Thrown in case that SMS verification fails. + * @throws DataAdapterRemoteException Thrown in case communication with remote system fails. + * @throws InvalidOperationContextException Thrown in case operation context is invalid. */ @RequestMapping(value = "verify", method = RequestMethod.POST) - public Response verifyAuthorizationSms(@RequestBody ObjectRequest request) throws SmsAuthorizationFailedException { - logger.info("Received verifyAuthorizationSms request, operation ID: "+request.getRequestObject().getOperationContext().getId()); + public ObjectResponse verifyAuthorizationSms(@RequestBody ObjectRequest request) throws InvalidOperationContextException, DataAdapterRemoteException { + logger.info("Received verifyAuthorizationSms request, operation ID: {}", request.getRequestObject().getOperationContext().getId()); VerifySmsAuthorizationRequest verifyRequest = request.getRequestObject(); + String userId = verifyRequest.getUserId(); + String organizationId = verifyRequest.getOrganizationId(); String messageId = verifyRequest.getMessageId(); String authorizationCode = verifyRequest.getAuthorizationCode(); + OperationContext operationContext = verifyRequest.getOperationContext(); // Verify authorization code - smsPersistenceService.verifyAuthorizationSms(messageId, authorizationCode, false); - logger.info("The verifyAuthorizationSms request succeeded, operation ID: "+request.getRequestObject().getOperationContext().getId()); - return new Response(); + VerifySmsAuthorizationResponse response = dataAdapter.verifyAuthorizationSms(userId, organizationId, messageId, authorizationCode, operationContext); + logger.info("The verifyAuthorizationSms request succeeded, operation ID: {}", request.getRequestObject().getOperationContext().getId()); + return new ObjectResponse<>(response); } /** @@ -144,28 +129,23 @@ public Response verifyAuthorizationSms(@RequestBody ObjectRequest request) throws SmsAuthorizationFailedException, AuthenticationFailedException, DataAdapterRemoteException { - logger.info("Received verifyAuthorizationSmsAndPassword request, operation ID: "+request.getRequestObject().getOperationContext().getId()); + public ObjectResponse verifyAuthorizationSmsAndPassword(@RequestBody ObjectRequest request) throws DataAdapterRemoteException, InvalidOperationContextException { + logger.info("Received verifyAuthorizationSmsAndPassword request, operation ID: {}", request.getRequestObject().getOperationContext().getId()); VerifySmsAndPasswordRequest verifyRequest = request.getRequestObject(); - // Verify authorization code - String messageId = verifyRequest.getMessageId(); - String authorizationCode = verifyRequest.getAuthorizationCode(); - smsPersistenceService.verifyAuthorizationSms(messageId, authorizationCode, true); - // Verify user password String userId = verifyRequest.getUserId(); - String password = verifyRequest.getPassword(); - AuthenticationType authenticationType = verifyRequest.getAuthenticationType(); - String cipherTransformation = verifyRequest.getCipherTransformation(); String organizationId = verifyRequest.getOrganizationId(); + String messageId = verifyRequest.getMessageId(); + String authorizationCode = verifyRequest.getAuthorizationCode(); OperationContext operationContext = verifyRequest.getOperationContext(); - dataAdapter.authenticateUser(userId, password, authenticationType, cipherTransformation, organizationId, operationContext); - logger.info("The verifyAuthorizationSmsAndPassword request succeeded, operation ID: "+request.getRequestObject().getOperationContext().getId()); - return new Response(); + String password = verifyRequest.getPassword(); + AuthenticationContext authenticationContext = verifyRequest.getAuthenticationContext(); + VerifySmsAndPasswordResponse response = dataAdapter.verifyAuthorizationSmsAndPassword(userId, organizationId, messageId, authorizationCode, operationContext, authenticationContext, password); + logger.info("The verifyAuthorizationSmsAndPassword request succeeded, operation ID: {}", request.getRequestObject().getOperationContext().getId()); + return new ObjectResponse<>(response); } } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/AuthenticationFailedException.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/AuthenticationFailedException.java deleted file mode 100644 index 7e49b0e6..00000000 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/AuthenticationFailedException.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2017 Wultra s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.getlime.security.powerauth.app.dataadapter.exception; - -/** - * Exception used for cases when authentication fails. - * - * @author Petr Dvorak, petr@wultra.com - */ -public class AuthenticationFailedException extends Exception { - - private Integer remainingAttempts; - - /** - * Default constructor. - */ - public AuthenticationFailedException() { - } - - /** - * Constructor with authentication failure message. - * @param message Authentication failure message. - */ - public AuthenticationFailedException(String message) { - super(message); - } - - /** - * Constructor with authentication failure message and cause. - * @param message Authentication failure message. - * @param cause Cause, original exception. - */ - public AuthenticationFailedException(String message, Throwable cause) { - super(message, cause); - } - - /** - * Constructor with cause. - * @param cause Cause, original exception. - */ - public AuthenticationFailedException(Throwable cause) { - super(cause); - } - - /** - * Get number of remaining authentication attempts. - * @return Number of remaining attempts. - */ - public Integer getRemainingAttempts() { - return remainingAttempts; - } - - /** - * Set number of remaining authentication attempts. - * @param remainingAttempts Number of remaining attempts. - */ - public void setRemainingAttempts(Integer remainingAttempts) { - this.remainingAttempts = remainingAttempts; - } -} diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/DefaultExceptionResolver.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/DefaultExceptionResolver.java index f5355d21..097e06a1 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/DefaultExceptionResolver.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/DefaultExceptionResolver.java @@ -55,35 +55,6 @@ public class DefaultExceptionResolver { return new ErrorResponse(error); } - /** - * Handling of authentication failures. - * @param ex Authentication failure exception, with exception details. - * @return Response with error information. - */ - @ExceptionHandler(AuthenticationFailedException.class) - @ResponseStatus(HttpStatus.UNAUTHORIZED) - public @ResponseBody ErrorResponse handleAuthenticationError(AuthenticationFailedException ex) { - // regular authentication failed error - DataAdapterError error = new DataAdapterError(DataAdapterError.Code.AUTHENTICATION_FAILED, ex.getMessage()); - error.setRemainingAttempts(ex.getRemainingAttempts()); - return new ErrorResponse(error); - } - - /** - * Handling of SMS OTP authorization failures. - * - * @param ex Authorization failure exception, with exception details. - * @return Response with error information. - */ - @ExceptionHandler(SmsAuthorizationFailedException.class) - @ResponseStatus(HttpStatus.UNAUTHORIZED) - public @ResponseBody ErrorResponse handleAuthenticationError(SmsAuthorizationFailedException ex) { - // regular sms authorization failed error - DataAdapterError error = new DataAdapterError(DataAdapterError.Code.SMS_AUTHORIZATION_FAILED, ex.getMessage()); - error.setRemainingAttempts(ex.getRemainingAttempts()); - return new ErrorResponse(error); - } - /** * Handling of validation errors. * @param ex Exception. diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/SmsAuthorizationFailedException.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/SmsAuthorizationFailedException.java deleted file mode 100644 index 6f10250d..00000000 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/SmsAuthorizationFailedException.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2017 Wultra s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.getlime.security.powerauth.app.dataadapter.exception; - -/** - * Exception used for cases when SMS OTP authorization fails. - * - * @author Roman Strobl, roman.strobl@wultra.com - */ -public class SmsAuthorizationFailedException extends Exception { - - private Integer remainingAttempts; - - /** - * Default constructor. - */ - public SmsAuthorizationFailedException() { - } - - /** - * Constructor with authorization failure message. - * - * @param message Authorization failure message. - */ - public SmsAuthorizationFailedException(String message) { - super(message); - } - - /** - * Constructor with authorization failure message and cause. - * - * @param message Authorization failure message. - * @param cause Cause, original exception. - */ - public SmsAuthorizationFailedException(String message, Throwable cause) { - super(message, cause); - } - - /** - * Constructor with cause. - * - * @param cause Cause, original exception. - */ - public SmsAuthorizationFailedException(Throwable cause) { - super(cause); - } - - /** - * Get number of remaining authentication attempts. - * @return Get remaining attempts. - */ - public Integer getRemainingAttempts() { - return remainingAttempts; - } - - /** - * Set number of remaining authentication attempts. - * @param remainingAttempts Number of remaining attempts. - */ - public void setRemainingAttempts(Integer remainingAttempts) { - this.remainingAttempts = remainingAttempts; - } -} diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index 9078d247..b46852ab 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -3,14 +3,18 @@ import io.getlime.security.powerauth.app.dataadapter.api.DataAdapter; import io.getlime.security.powerauth.app.dataadapter.exception.*; import io.getlime.security.powerauth.app.dataadapter.service.DataAdapterI18NService; +import io.getlime.security.powerauth.app.dataadapter.service.SmsPersistenceService; import io.getlime.security.powerauth.crypto.server.util.DataDigest; import io.getlime.security.powerauth.lib.dataadapter.model.entity.*; import io.getlime.security.powerauth.lib.dataadapter.model.entity.attribute.AmountAttribute; import io.getlime.security.powerauth.lib.dataadapter.model.entity.attribute.FormFieldConfig; -import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.AuthenticationType; +import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.PasswordProtectionType; +import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.SmsAuthorizationResult; +import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.UserAuthenticationResult; import io.getlime.security.powerauth.lib.dataadapter.model.response.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.stereotype.Service; @@ -18,6 +22,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.UUID; /** * Sample implementation of DataAdapter interface which should be updated in real implementation. @@ -33,10 +38,13 @@ public class DataAdapterService implements DataAdapter { private final DataAdapterI18NService dataAdapterI18NService; private final OperationValueExtractionService operationValueExtractionService; + private final SmsPersistenceService smsPersistenceService; - public DataAdapterService(DataAdapterI18NService dataAdapterI18NService, OperationValueExtractionService operationValueExtractionService) { + @Autowired + public DataAdapterService(DataAdapterI18NService dataAdapterI18NService, OperationValueExtractionService operationValueExtractionService, SmsPersistenceService smsPersistenceService) { this.dataAdapterI18NService = dataAdapterI18NService; this.operationValueExtractionService = operationValueExtractionService; + this.smsPersistenceService = smsPersistenceService; } @Override @@ -46,28 +54,38 @@ public UserDetailResponse lookupUser(String username, String organizationId, Ope } @Override - public UserDetailResponse authenticateUser(String userId, String password, AuthenticationType authenticationType, String cipherTransformation, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, AuthenticationFailedException { + public UserAuthenticationResponse authenticateUser(String userId, String password, AuthenticationContext authenticationContext, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException { // Here will be the real authentication - call to the backend providing authentication. // In case that authentication fails, throw an AuthenticationFailedException. - if (authenticationType == AuthenticationType.BASIC && "test".equals(password)) { + PasswordProtectionType passwordProtection = authenticationContext.getPasswordProtection(); + UserAuthenticationResponse authResponse = new UserAuthenticationResponse(); + if (passwordProtection == PasswordProtectionType.NO_PROTECTION && "test".equals(password)) { try { - UserDetailResponse response = fetchUserDetail(userId, organizationId, operationContext); + UserDetailResponse userDetail = fetchUserDetail(userId, organizationId, operationContext); // The organization needs to be set in response (e.g. client authenticated against RETAIL organization or SME organization). - response.setOrganizationId(organizationId); - return response; + userDetail.setOrganizationId(organizationId); + authResponse.setUserDetail(userDetail); + authResponse.setAuthenticationResult(UserAuthenticationResult.VERIFIED_SUCCEEDED); + return authResponse; } catch (UserNotFoundException e) { - throw new AuthenticationFailedException("login.authenticationFailed"); + authResponse.setAuthenticationResult(UserAuthenticationResult.VERIFIED_FAILED); + authResponse.setErrorMessage("login.authenticationFailed"); + return authResponse; } } - AuthenticationFailedException authFailedException = new AuthenticationFailedException("login.authenticationFailed"); - // Set number of remaining attempts for this userId in case it is available. - // authFailedException.setRemainingAttempts(5); + authResponse.setAuthenticationResult(UserAuthenticationResult.VERIFIED_FAILED); + authResponse.setErrorMessage("login.authenticationFailed"); + // Set number of remaining attempts for this user ID in case it is available. + // authResponse.setRemainingAttempts(5); + + // To enable showing of remaining attempts for operation, use: + // authResponse.setShowRemainingAttempts(true); // Use the following code to let the user know that the account has been blocked temporarily. - // final AuthenticationFailedException authFailedException = new AuthenticationFailedException("login.authenticationBlocked"); - // authFailedException.setRemainingAttempts(0); + // authResponse.setErrorMessage("login.authenticationBlocked"); + // authResponse.setRemainingAttempts(0); - throw authFailedException; + return authResponse; } @Override @@ -169,6 +187,27 @@ public void operationChangedNotification(String userId, String organizationId, O logger.info("Operation changed, status: {}, operation ID: {}", change.toString(), operationId); } + @Override + public String createAuthorizationSms(String userId, String organizationId, OperationContext operationContext, String lang) throws InvalidOperationContextException, DataAdapterRemoteException { + // messageId is generated as random UUID, it can be overridden to provide a real message identification + String messageId = UUID.randomUUID().toString(); + + // generate authorization code + AuthorizationCode authorizationCode = generateAuthorizationCode(userId, organizationId, operationContext); + + // generate message text, include previously generated authorization code + String messageText = generateSmsText(userId, organizationId, operationContext, authorizationCode, lang); + + // persist authorization SMS message + smsPersistenceService.createAuthorizationSms(userId, organizationId, messageId, operationContext, authorizationCode, messageText); + + // Send SMS with generated text to target user. + sendAuthorizationSms(userId, organizationId, messageId, messageText, operationContext); + + // return generated message ID + return messageId; + } + @Override public AuthorizationCode generateAuthorizationCode(String userId, String organizationId, OperationContext operationContext) throws InvalidOperationContextException { String operationName = operationContext.getName(); @@ -230,9 +269,51 @@ public String generateSmsText(String userId, String organizationId, OperationCon } @Override - public void sendAuthorizationSms(String userId, String organizationId, String messageText, OperationContext operationContext) throws DataAdapterRemoteException, SmsAuthorizationFailedException { + public void sendAuthorizationSms(String userId, String organizationId, String messageId, String messageText, OperationContext operationContext) throws DataAdapterRemoteException { // Add here code to send the SMS OTP message to user identified by userId with messageText. - // In case message delivery fails, throw an SmsAuthorizationFailedException. + // The message entity can be extracted using message ID from table da_sms_authorization. + // In case message delivery fails, throw a DataAdapterRemoteException. + } + + @Override + public VerifySmsAuthorizationResponse verifyAuthorizationSms(String userId, String organizationId, String messageId, String authorizationCode, OperationContext operationContext) throws DataAdapterRemoteException, InvalidOperationContextException { + // You can override this logic in case more complex handling of SMS verification si required. + VerifySmsAuthorizationResponse response = smsPersistenceService.verifyAuthorizationSms(messageId, authorizationCode, false); + // You can enable showing of remaining attempts for the operation. + // response.setShowRemainingAttempts(true); + return response; + } + + @Override + public VerifySmsAndPasswordResponse verifyAuthorizationSmsAndPassword(String userId, String organizationId, String messageId, String authorizationCode, OperationContext operationContext, AuthenticationContext authenticationContext, String password) throws DataAdapterRemoteException, InvalidOperationContextException { + VerifySmsAndPasswordResponse response = new VerifySmsAndPasswordResponse(); + + // Verify authorization code from SMS + VerifySmsAuthorizationResponse smsResponse = smsPersistenceService.verifyAuthorizationSms(messageId, authorizationCode, true); + authenticationContext.setSmsAuthorizationResult(smsResponse.getSmsAuthorizationResult()); + + // Authenticate user + UserAuthenticationResponse authResponse = authenticateUser(userId, password, authenticationContext, organizationId, operationContext); + + // Create aggregate response + response.setSmsAuthorizationResult(smsResponse.getSmsAuthorizationResult()); + response.setUserAuthenticationResult(authResponse.getAuthenticationResult()); + if (smsResponse.getSmsAuthorizationResult() != SmsAuthorizationResult.VERIFIED_SUCCEEDED + || authResponse.getAuthenticationResult() != UserAuthenticationResult.VERIFIED_SUCCEEDED) { + // Provide an error message which does not allow to find out reason of failed verification. + response.setErrorMessage("login.authenticationFailed"); + } + // Optionally set the number of remaining attempts, e.g. using lower of the two remaining attempt counts. + // response.setRemainingAttempts(Math.min(smsResponse.getRemainingAttempts(), authResponse.getRemainingAttempts())); + // You can enable showing of remaining attempts for the operation. + // response.setShowRemainingAttempts(true); + return response; + } + + @Override + public InitConsentFormResponse initConsentForm(String userId, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, InvalidOperationContextException { + // Override this logic in case consent form should be displayed conditionally for given operation context. + return new InitConsentFormResponse(true); } @Override diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java index 445279ab..ac188805 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java @@ -16,9 +16,10 @@ package io.getlime.security.powerauth.app.dataadapter.impl.validation; import io.getlime.core.rest.model.base.request.ObjectRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.entity.AuthenticationContext; import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; -import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.AuthenticationType; -import io.getlime.security.powerauth.lib.dataadapter.model.request.AuthenticationRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.PasswordProtectionType; +import io.getlime.security.powerauth.lib.dataadapter.model.request.UserAuthenticationRequest; import io.getlime.security.powerauth.lib.dataadapter.model.request.UserLookupRequest; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; @@ -79,8 +80,8 @@ public void validate(@Nullable Object o, @NonNull Errors errors) { if (organizationId != null && organizationId.length() > 256) { errors.rejectValue("requestObject.organizationId", "login.organizationId.long"); } - } else if (objectRequest.getRequestObject() instanceof AuthenticationRequest) { - AuthenticationRequest authRequest = (AuthenticationRequest) objectRequest.getRequestObject(); + } else if (objectRequest.getRequestObject() instanceof UserAuthenticationRequest) { + UserAuthenticationRequest authRequest = (UserAuthenticationRequest) objectRequest.getRequestObject(); // update validation logic based on the real Data Adapter requirements String userId = authRequest.getUserId(); @@ -95,9 +96,10 @@ public void validate(@Nullable Object o, @NonNull Errors errors) { errors.rejectValue("requestObject.userId", "login.userId.long"); } - AuthenticationType authType = authRequest.getAuthenticationType(); ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.password", "login.password.empty"); - if (authType == AuthenticationType.BASIC) { + AuthenticationContext authenticationContext = authRequest.getAuthenticationContext(); + PasswordProtectionType passwordProtection = authenticationContext.getPasswordProtection(); + if (passwordProtection == PasswordProtectionType.NO_PROTECTION) { if (password != null && password.length() > 30) { errors.rejectValue("requestObject.password", "login.password.long"); } @@ -113,8 +115,8 @@ public void validate(@Nullable Object o, @NonNull Errors errors) { errors.rejectValue("requestObject.organizationId", "login.organizationId.long"); } - if (authType != AuthenticationType.BASIC && authType != AuthenticationType.PASSWORD_ENCRYPTION_AES) { - errors.rejectValue("requestObject.authenticationType", "login.type.unsupported"); + if (passwordProtection != PasswordProtectionType.NO_PROTECTION && passwordProtection != PasswordProtectionType.PASSWORD_ENCRYPTION_AES) { + errors.rejectValue("requestObject.authenticationContext", "login.type.unsupported"); } } } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/ConsentFormRequestValidator.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/ConsentFormRequestValidator.java index 2e80f5b4..7eb606ab 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/ConsentFormRequestValidator.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/ConsentFormRequestValidator.java @@ -19,6 +19,7 @@ import io.getlime.security.powerauth.lib.dataadapter.model.entity.ConsentOption; import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; import io.getlime.security.powerauth.lib.dataadapter.model.request.CreateConsentFormRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.request.InitConsentFormRequest; import io.getlime.security.powerauth.lib.dataadapter.model.request.SaveConsentFormRequest; import io.getlime.security.powerauth.lib.dataadapter.model.request.ValidateConsentFormRequest; import org.springframework.lang.NonNull; @@ -65,7 +66,15 @@ public void validate(@Nullable Object o, @NonNull Errors errors) { } // update validation logic based on the real Data Adapter requirements - if (objectRequest.getRequestObject() instanceof CreateConsentFormRequest) { + if (objectRequest.getRequestObject() instanceof InitConsentFormRequest) { + ObjectRequest requestObject = (ObjectRequest) o; + InitConsentFormRequest request = requestObject.getRequestObject(); + validateOperationContext(request.getOperationContext(), errors); + validateUserId(request.getUserId(), errors); + if (request.getOperationContext() != null) { + validateOperationName(request.getOperationContext().getName(), errors); + } + } else if (objectRequest.getRequestObject() instanceof CreateConsentFormRequest) { ObjectRequest requestObject = (ObjectRequest) o; CreateConsentFormRequest request = requestObject.getRequestObject(); validateOperationContext(request.getOperationContext(), errors); diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SmsPersistenceService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SmsPersistenceService.java index b1cc4ca4..80cfd001 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SmsPersistenceService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SmsPersistenceService.java @@ -17,19 +17,18 @@ import io.getlime.security.powerauth.app.dataadapter.configuration.DataAdapterConfiguration; import io.getlime.security.powerauth.app.dataadapter.exception.InvalidOperationContextException; -import io.getlime.security.powerauth.app.dataadapter.exception.SmsAuthorizationFailedException; -import io.getlime.security.powerauth.app.dataadapter.impl.service.DataAdapterService; import io.getlime.security.powerauth.app.dataadapter.repository.SmsAuthorizationRepository; import io.getlime.security.powerauth.app.dataadapter.repository.model.entity.SmsAuthorizationEntity; import io.getlime.security.powerauth.lib.dataadapter.model.entity.AuthorizationCode; import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; +import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.SmsAuthorizationResult; +import io.getlime.security.powerauth.lib.dataadapter.model.response.VerifySmsAuthorizationResponse; import org.joda.time.DateTime; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Date; import java.util.Optional; -import java.util.UUID; /** * Service class for generating SMS with OTP authorization code and verification of authorization code. @@ -39,19 +38,16 @@ @Service public class SmsPersistenceService { - private final DataAdapterService dataAdapterService; private final SmsAuthorizationRepository smsAuthorizationRepository; private final DataAdapterConfiguration dataAdapterConfiguration; /** * SMS persistence service constructor. - * @param dataAdapterService Data adapter service. * @param smsAuthorizationRepository SMS authorization repository. * @param dataAdapterConfiguration Data adapter configuration. */ @Autowired - public SmsPersistenceService(DataAdapterService dataAdapterService, SmsAuthorizationRepository smsAuthorizationRepository, DataAdapterConfiguration dataAdapterConfiguration) { - this.dataAdapterService = dataAdapterService; + public SmsPersistenceService(SmsAuthorizationRepository smsAuthorizationRepository, DataAdapterConfiguration dataAdapterConfiguration) { this.smsAuthorizationRepository = smsAuthorizationRepository; this.dataAdapterConfiguration = dataAdapterConfiguration; } @@ -60,29 +56,22 @@ public SmsPersistenceService(DataAdapterService dataAdapterService, SmsAuthoriza * Create an authorization SMS message with OTP authorization code. * @param userId User ID. * @param organizationId Organization ID. + * @param messageId Message ID * @param operationContext Operation context. - * @param lang Language for message text. + * @param authorizationCode Authorization code for SMS message. + * @param messageText Localized SMS message text. * @return Created entity with SMS message details. + * @throws InvalidOperationContextException In case operation context is invalid. */ - public SmsAuthorizationEntity createAuthorizationSms(String userId, String organizationId, OperationContext operationContext, String lang) throws InvalidOperationContextException { - String operationId = operationContext.getId(); - String operationName = operationContext.getName(); - - // messageId is generated as random UUID, it can be overridden to provide a real message identification - String messageId = UUID.randomUUID().toString(); - - // generate authorization code - AuthorizationCode authorizationCode = dataAdapterService.generateAuthorizationCode(userId, organizationId, operationContext); - - // generate message text, include previously generated authorization code - String messageText = dataAdapterService.generateSmsText(userId, organizationId, operationContext, authorizationCode, lang); + public SmsAuthorizationEntity createAuthorizationSms(String userId, String organizationId, String messageId, OperationContext operationContext, + AuthorizationCode authorizationCode, String messageText) throws InvalidOperationContextException { SmsAuthorizationEntity smsEntity = new SmsAuthorizationEntity(); smsEntity.setMessageId(messageId); - smsEntity.setOperationId(operationId); + smsEntity.setOperationId(operationContext.getId()); smsEntity.setUserId(userId); smsEntity.setOrganizationId(organizationId); - smsEntity.setOperationName(operationName); + smsEntity.setOperationName(operationContext.getName()); smsEntity.setAuthorizationCode(authorizationCode.getCode()); smsEntity.setSalt(authorizationCode.getSalt()); smsEntity.setMessageText(messageText); @@ -99,16 +88,18 @@ public SmsAuthorizationEntity createAuthorizationSms(String userId, String organ } /** - * Verify an OTP authorization code. + * Verify an authorization code from SMS message. * @param messageId Message ID. * @param authorizationCode Authorization code. - * @param smsAndPasswordCombined Whether SMS code is used together with password. - * @throws SmsAuthorizationFailedException Thrown when SMS authorization fails. + * @param allowMultipleVerifications Whether authorization code can be verified multiple times. */ - public void verifyAuthorizationSms(String messageId, String authorizationCode, boolean smsAndPasswordCombined) throws SmsAuthorizationFailedException { + public VerifySmsAuthorizationResponse verifyAuthorizationSms(String messageId, String authorizationCode, boolean allowMultipleVerifications) { Optional smsEntityOptional = smsAuthorizationRepository.findById(messageId); + VerifySmsAuthorizationResponse response = new VerifySmsAuthorizationResponse(); if (!smsEntityOptional.isPresent()) { - throw new SmsAuthorizationFailedException("smsAuthorization.invalidMessage"); + response.setSmsAuthorizationResult(SmsAuthorizationResult.VERIFIED_FAILED); + response.setErrorMessage("smsAuthorization.invalidMessage"); + return response; } SmsAuthorizationEntity smsEntity = smsEntityOptional.get(); // increase number of verification tries and save entity @@ -118,36 +109,41 @@ public void verifyAuthorizationSms(String messageId, String authorizationCode, b final Integer remainingAttempts = dataAdapterConfiguration.getSmsOtpMaxVerifyTriesPerMessage() - smsEntity.getVerifyRequestCount(); if (smsEntity.getAuthorizationCode() == null || smsEntity.getAuthorizationCode().isEmpty()) { - SmsAuthorizationFailedException ex = new SmsAuthorizationFailedException("smsAuthorization.invalidCode"); - ex.setRemainingAttempts(remainingAttempts); - throw ex; + response.setSmsAuthorizationResult(SmsAuthorizationResult.VERIFIED_FAILED); + response.setRemainingAttempts(remainingAttempts); + response.setErrorMessage("smsAuthorization.invalidCode"); + return response; } if (smsEntity.isExpired()) { - throw new SmsAuthorizationFailedException("smsAuthorization.expired"); + response.setSmsAuthorizationResult(SmsAuthorizationResult.VERIFIED_FAILED); + response.setErrorMessage("smsAuthorization.expired"); + return response; } - if (!smsAndPasswordCombined && smsEntity.isVerified()) { - throw new SmsAuthorizationFailedException("smsAuthorization.alreadyVerified"); + if (!allowMultipleVerifications && smsEntity.isVerified()) { + response.setSmsAuthorizationResult(SmsAuthorizationResult.VERIFIED_FAILED); + response.setErrorMessage("smsAuthorization.alreadyVerified"); + return response; } if (smsEntity.getVerifyRequestCount() > dataAdapterConfiguration.getSmsOtpMaxVerifyTriesPerMessage()) { - throw new SmsAuthorizationFailedException("smsAuthorization.maxAttemptsExceeded"); + response.setSmsAuthorizationResult(SmsAuthorizationResult.VERIFIED_FAILED); + response.setErrorMessage("smsAuthorization.maxAttemptsExceeded"); + return response; } String authorizationCodeExpected = smsEntity.getAuthorizationCode(); if (!authorizationCode.equals(authorizationCodeExpected)) { - if (smsAndPasswordCombined) { - // Use authentication error so that attacker cannot determine whether password or SMS code was invalid - SmsAuthorizationFailedException ex = new SmsAuthorizationFailedException("login.authenticationFailed"); - ex.setRemainingAttempts(remainingAttempts); - throw ex; - } - SmsAuthorizationFailedException ex = new SmsAuthorizationFailedException("smsAuthorization.failed"); - ex.setRemainingAttempts(remainingAttempts); - throw ex; + response.setSmsAuthorizationResult(SmsAuthorizationResult.VERIFIED_FAILED); + response.setRemainingAttempts(remainingAttempts); + response.setErrorMessage("smsAuthorization.failed"); + return response; } // SMS OTP authorization succeeded when this line is reached, update entity verification status smsEntity.setVerified(true); smsEntity.setTimestampVerified(new Date()); smsAuthorizationRepository.save(smsEntity); + + response.setSmsAuthorizationResult(SmsAuthorizationResult.VERIFIED_SUCCEEDED); + return response; } } From 52d83811d4d90f7f72541c4703148e6531581da7 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Fri, 12 Jul 2019 20:24:51 +0200 Subject: [PATCH 35/79] Update code comments --- .../app/dataadapter/impl/service/DataAdapterService.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index b46852ab..13d3c7eb 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -56,7 +56,8 @@ public UserDetailResponse lookupUser(String username, String organizationId, Ope @Override public UserAuthenticationResponse authenticateUser(String userId, String password, AuthenticationContext authenticationContext, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException { // Here will be the real authentication - call to the backend providing authentication. - // In case that authentication fails, throw an AuthenticationFailedException. + // Return a response with UserAuthenticationResult based on the actual authentication result. + // The password is optionally encrypted, the authentication context contains information about encryption. PasswordProtectionType passwordProtection = authenticationContext.getPasswordProtection(); UserAuthenticationResponse authResponse = new UserAuthenticationResponse(); if (passwordProtection == PasswordProtectionType.NO_PROTECTION && "test".equals(password)) { @@ -279,6 +280,8 @@ public void sendAuthorizationSms(String userId, String organizationId, String me public VerifySmsAuthorizationResponse verifyAuthorizationSms(String userId, String organizationId, String messageId, String authorizationCode, OperationContext operationContext) throws DataAdapterRemoteException, InvalidOperationContextException { // You can override this logic in case more complex handling of SMS verification si required. VerifySmsAuthorizationResponse response = smsPersistenceService.verifyAuthorizationSms(messageId, authorizationCode, false); + // Set number of remaining attempts for verification in case it is available. + // authResponse.setRemainingAttempts(5); // You can enable showing of remaining attempts for the operation. // response.setShowRemainingAttempts(true); return response; From 874b562f1c22c08d0e7bc14ecc82aee29e918d16 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Fri, 12 Jul 2019 22:15:30 +0200 Subject: [PATCH 36/79] Update JavaDoc --- .../security/powerauth/app/dataadapter/api/DataAdapter.java | 1 + 1 file changed, 1 insertion(+) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java index 53977835..fc51c348 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java @@ -98,6 +98,7 @@ public interface DataAdapter { * @param organizationId Organization ID. * @param operationContext Operation context. * @param lang Language for localization. + * @return Message ID. * @throws InvalidOperationContextException Thrown when operation context is invalid. * @throws DataAdapterRemoteException Thrown when remote communication fails or SMS message could not be delivered. */ From 12a490fe8f68aac5f21c06b34a840474809d6f18 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Fri, 12 Jul 2019 22:43:51 +0200 Subject: [PATCH 37/79] Fix logged message --- .../app/dataadapter/controller/AuthenticationController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java index e09123e9..e80fef25 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java @@ -86,7 +86,7 @@ public ObjectResponse lookupUser(@Valid @RequestBody ObjectR String organizationId = lookupRequest.getOrganizationId(); OperationContext operationContext = lookupRequest.getOperationContext(); UserDetailResponse response = dataAdapter.lookupUser(username, organizationId, operationContext); - logger.info("The user lookup request succeeded, user name: {}, organization ID: {}, operation ID: {}", response.getId(), response.getOrganizationId(), request.getRequestObject().getOperationContext().getId()); + logger.info("The user lookup request succeeded, user ID: {}, organization ID: {}, operation ID: {}", response.getId(), response.getOrganizationId(), request.getRequestObject().getOperationContext().getId()); return new ObjectResponse<>(response); } From 69f21b3a1e0a45c0c9c29792f5024c47d13aa75e Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Fri, 12 Jul 2019 22:55:36 +0200 Subject: [PATCH 38/79] Support for fake SMS message delivery in case user account does not exist or it is blocked --- .../app/dataadapter/impl/service/DataAdapterService.java | 5 +++++ .../validation/CreateSmsAuthorizationRequestValidator.java | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index 13d3c7eb..827ae373 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -193,6 +193,11 @@ public String createAuthorizationSms(String userId, String organizationId, Opera // messageId is generated as random UUID, it can be overridden to provide a real message identification String messageId = UUID.randomUUID().toString(); + // fake SMS message delivery for null user ID + if (userId == null) { + return messageId; + } + // generate authorization code AuthorizationCode authorizationCode = generateAuthorizationCode(userId, organizationId, operationContext); diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSmsAuthorizationRequestValidator.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSmsAuthorizationRequestValidator.java index eb68f408..62f523a9 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSmsAuthorizationRequestValidator.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSmsAuthorizationRequestValidator.java @@ -87,12 +87,12 @@ public void validate(@Nullable Object o, @NonNull Errors errors) { String operationName = authRequest.getOperationContext().getName(); - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.userId", "smsAuthorization.userId.empty"); + // Allow null user ID for case when fake SMS message is sent if (userId != null && userId.length() > 30) { errors.rejectValue("requestObject.userId", "smsAuthorization.userId.long"); } - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.organizationId", "smsAuthorization.organizationId.empty"); + // Allow null organization ID for case when fake SMS message is sent if (organizationId != null && organizationId.length() > 256) { errors.rejectValue("requestObject.organizationId", "smsAuthorization.organizationId.long"); } From 905705d5db94ae3355a8890a6f3aeca8d9a84633 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Fri, 12 Jul 2019 23:14:56 +0200 Subject: [PATCH 39/79] Added comment about fake SMS delivery with null user ID used instead of an exception --- .../app/dataadapter/impl/service/DataAdapterService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index 827ae373..99db79c8 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -50,6 +50,8 @@ public DataAdapterService(DataAdapterI18NService dataAdapterI18NService, Operati @Override public UserDetailResponse lookupUser(String username, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, UserNotFoundException { // The sample Data Adapter code uses 1:1 mapping of username to userId. In real implementation the userId usually differs from the username, so translation of username to user ID is required. + // If user does not exist or user account is blocked and such error needs to be silent, return null values for user ID and organization ID. + // The SCA login moves fakes SMS message delivery even for case when user ID is null to disallow fishing of usernames. return fetchUserDetail(username, organizationId, operationContext); } @@ -95,7 +97,7 @@ public UserDetailResponse fetchUserDetail(String userId, String organizationId, // In case that user is not found, throw a UserNotFoundException. // The operation context may be null in case the method is called outside of an active operation (e.g. OAuth user profile request). UserDetailResponse responseObject = new UserDetailResponse(); - responseObject.setId(userId); + responseObject.setId(null); responseObject.setGivenName("John"); responseObject.setFamilyName("Doe"); responseObject.setOrganizationId(organizationId); From 3896a2ed04d43ea614c9843b10b1c947ec4d4072 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Fri, 12 Jul 2019 23:16:38 +0200 Subject: [PATCH 40/79] Comments related to error handling --- .../app/dataadapter/impl/service/DataAdapterService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index 99db79c8..ca7992f1 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -51,7 +51,8 @@ public DataAdapterService(DataAdapterI18NService dataAdapterI18NService, Operati public UserDetailResponse lookupUser(String username, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, UserNotFoundException { // The sample Data Adapter code uses 1:1 mapping of username to userId. In real implementation the userId usually differs from the username, so translation of username to user ID is required. // If user does not exist or user account is blocked and such error needs to be silent, return null values for user ID and organization ID. - // The SCA login moves fakes SMS message delivery even for case when user ID is null to disallow fishing of usernames. + // The SCA login fakes SMS message delivery even for case when user ID is null to disallow fishing of usernames. + // For case when an error should appear instead, throw a UserNotFoundException. return fetchUserDetail(username, organizationId, operationContext); } From 4dc14e1c099b5c7084a12a163faaf6a97e921a65 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Fri, 12 Jul 2019 23:18:53 +0200 Subject: [PATCH 41/79] Fixed typo --- .../app/dataadapter/impl/service/DataAdapterService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index ca7992f1..176bd00b 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -286,7 +286,7 @@ public void sendAuthorizationSms(String userId, String organizationId, String me @Override public VerifySmsAuthorizationResponse verifyAuthorizationSms(String userId, String organizationId, String messageId, String authorizationCode, OperationContext operationContext) throws DataAdapterRemoteException, InvalidOperationContextException { - // You can override this logic in case more complex handling of SMS verification si required. + // You can override this logic in case more complex handling of SMS verification is required. VerifySmsAuthorizationResponse response = smsPersistenceService.verifyAuthorizationSms(messageId, authorizationCode, false); // Set number of remaining attempts for verification in case it is available. // authResponse.setRemainingAttempts(5); From 7d4da7aa89cfc4d69857e2cfe793ce35465a3d55 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Fri, 12 Jul 2019 23:20:02 +0200 Subject: [PATCH 42/79] Revert test change --- .../app/dataadapter/impl/service/DataAdapterService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index 176bd00b..04ee0020 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -98,7 +98,7 @@ public UserDetailResponse fetchUserDetail(String userId, String organizationId, // In case that user is not found, throw a UserNotFoundException. // The operation context may be null in case the method is called outside of an active operation (e.g. OAuth user profile request). UserDetailResponse responseObject = new UserDetailResponse(); - responseObject.setId(null); + responseObject.setId(userId); responseObject.setGivenName("John"); responseObject.setFamilyName("Doe"); responseObject.setOrganizationId(organizationId); From 5e67c9e60142df670408e03ea4637582628ec4be Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Sat, 13 Jul 2019 13:48:10 +0200 Subject: [PATCH 43/79] Added comment related verification of SMS authorization with password --- .../app/dataadapter/impl/service/DataAdapterService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index 04ee0020..da757004 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -61,6 +61,8 @@ public UserAuthenticationResponse authenticateUser(String userId, String passwor // Here will be the real authentication - call to the backend providing authentication. // Return a response with UserAuthenticationResult based on the actual authentication result. // The password is optionally encrypted, the authentication context contains information about encryption. + // In case of combined user authentication with SMS authorization the authentication context contains information + // about result of SMS authorization. PasswordProtectionType passwordProtection = authenticationContext.getPasswordProtection(); UserAuthenticationResponse authResponse = new UserAuthenticationResponse(); if (passwordProtection == PasswordProtectionType.NO_PROTECTION && "test".equals(password)) { From e326bd5d4a882d46a5aeb24b88192bb2b4830a75 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Sat, 13 Jul 2019 15:04:59 +0200 Subject: [PATCH 44/79] Replace String by constant --- .../app/dataadapter/impl/service/DataAdapterService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index da757004..4c84f03f 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -154,7 +154,7 @@ public DecorateOperationFormDataResponse decorateFormData(String userId, String List configs = formData.getConfig(); for (FormFieldConfig config: configs) { - if ("operation.bankAccountChoice".equals(config.getId())) { + if (BANK_ACCOUNT_CHOICE_ID.equals(config.getId())) { choiceEnabled = config.isEnabled(); // You should check the default value against list of available accounts. defaultValue = config.getDefaultValue(); From 8fd5d9be2df8019712eb4dcd10b29236c7a9aa0f Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Sun, 14 Jul 2019 13:24:48 +0200 Subject: [PATCH 45/79] Fix #92: Review and update documentation --- docs/Customizing-Web-Flow-Appearance.md | 13 +++-- ...Implementing-the-Data-Adapter-Interface.md | 50 +++++++++++-------- .../app/dataadapter/api/DataAdapter.java | 6 +-- .../impl/service/DataAdapterService.java | 5 +- .../SmsAuthorizationRepository.java | 4 +- 5 files changed, 48 insertions(+), 30 deletions(-) diff --git a/docs/Customizing-Web-Flow-Appearance.md b/docs/Customizing-Web-Flow-Appearance.md index d9c98e27..00b66fca 100644 --- a/docs/Customizing-Web-Flow-Appearance.md +++ b/docs/Customizing-Web-Flow-Appearance.md @@ -63,8 +63,15 @@ After you make a copy of the `powerauth-webflow-customization` project, you can The OAuth 2.0 consent form used by Web Flow can be customized by implementing following methods from Data Adapter interface: +### Initialize Consent Form + +The [initConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L177) method is used to +allow to decide whether consent form for should be displayed for given operation context. Based on values of parameters `userId`, `organizationId` +and `operationContext` a decision can be made whether to display the consent form or not. In case the consent form is always displayed, +return true in response unconditionally. + ### Create Consent Form -The [createConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L119) method is used to specify +The [createConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L189) method is used to specify the text of consent form and define options which are available in the options form. The consent form accepts consent text as HTML, scripting of the HTML is not allowed. The language of the consent form is specified using parameter `lang`. Each option is identified using an identifier `id`. Individual options in the form can be set as required and their default value can be set. The form can use parameters `userId`, `organizationId` and `operationContext` including `name`, `formData` and `applicationContext` to create a customized and personalized consent form for given @@ -82,7 +89,7 @@ The response should contain following data: _Note that the consent texts do not use automatic resource localization because the HTML texts are expected to be complex and dynamically generated._ ### Validate Consent Form -The [validateConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L132) method is used to validate the OAuth 2.0 consent form options +The [validateConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L203) method is used to validate the OAuth 2.0 consent form options before the response is persisted. The identifiers of consent options match identifiers created in the `createConsentForm` step. The error messages produced by this method should take into account language specified using parameter `lang`. @@ -97,7 +104,7 @@ The response should contain following data: _Note that the texts of error messages do not use automatic resource localization because the HTML texts are expected to be complex and dynamically generated._ ### Save Consent Form -The [saveConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L143) method is used to save the OAuth 2.0 consent form options. +The [saveConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L215) method is used to save the OAuth 2.0 consent form options. This method is called only when form validation done in `validateConsentForm` method successfully passes. The sample implementation prints the consent form option values into log. It is expected that in the real implementation the consent option values are persisted in a database or any other persistent storage of consent options. \ No newline at end of file diff --git a/docs/Implementing-the-Data-Adapter-Interface.md b/docs/Implementing-the-Data-Adapter-Interface.md index a0c3e3e7..d0230c66 100644 --- a/docs/Implementing-the-Data-Adapter-Interface.md +++ b/docs/Implementing-the-Data-Adapter-Interface.md @@ -7,18 +7,22 @@ Furthermore, the Data Adapter can be used to customize text and options for the The interface methods are defined in the [DataAdapter interface](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java): -- [lookupUser](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L38) - lookup user account based on username -- [authenticateUser](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L52) - perform user authentication with remote backend based on provided credentials -- [fetchUserDetail](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L63) - retrieve user details for given user ID -- [decorateFormData](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L74) - retrieve operation form data and decorate it -- [formDataChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L84) - method is called when operation form data changes to allow notification of client backends -- [operationChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L94) - method is called when operation status changes to allow notification of client backends -- [generateAuthorizationCode](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L104) - generate authorization code for authorization SMS message -- [generateSmsText](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L116) - generate SMS text for authorization SMS message -- [sendAuthorizationSms](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L127) - send authorization SMS message -- [createConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L139) - create an OAuth 2.0 consent form -- [validateConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L153) - validate the OAuth 2.0 consent form options -- [saveConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L165) - save the OAuth 2.0 consent form options +- [lookupUser](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L39) - lookup user account based on username +- [authenticateUser](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L51) - perform user authentication with remote backend based on provided credentials +- [fetchUserDetail](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L62) - retrieve user details for given user ID +- [decorateFormData](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L73) - retrieve operation form data and decorate it +- [formDataChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L83) - method is called when operation form data changes to allow notification of client backends +- [operationChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L93) - method is called when operation status changes to allow notification of client backends +- [createAuthorizationSms](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L105) - create and send authorization SMS message +- [generateAuthorizationCode](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L115) - generate authorization code for authorization SMS message +- [generateSmsText](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L127) - generate SMS text for authorization SMS message +- [sendAuthorizationSms](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L138) - send authorization SMS message +- [verifyAuthorizationSms](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L151) - verify authorization code from SMS message +- [verifyAuthorizationSmsAndPassword](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L166) - verify authorization code from SMS message together with verifying user password +- [initConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L177) - initialize the OAuth 2.0 consent form and decide whether consent form should be displayed +- [createConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L189) - create the OAuth 2.0 consent form text and options +- [validateConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L203) - validate the OAuth 2.0 consent form options +- [saveConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L215) - save the OAuth 2.0 consent form options selected by the user ## Customizing Data Adapter @@ -28,15 +32,19 @@ Following steps are required for customization of Data Adapter. Consider which of the following methods need to be implemented in your project: -- [lookupUser](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L38) - (required) - provides mapping of username to user ID which is used by other methods -- [authenticateUser](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L52) (optional) - implementation is required in case any Web Flow operation needs to authenticate the user using a username/password login form -- [fetchUserDetail](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L63) (required) - provides information about the user (user ID and name) for the OAuth 2.0 protocol -- [decorateFormData](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L74) (optional) - implementation is required in case any Web Flow operation form data needs to be updated after authentication (e.g. add information about user bank accounts) -- [formDataChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L84) (optional) - implementation is required in case the client backends need to be notified about user input during an operation -- [operationChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L94) (optional) - implementation is required in case the client backends need to be notified about operation status changes -- [generateAuthorizationCode](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L104) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization -- [generateSmsText](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L116) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization -- [sendAuthorizationSms](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L127) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization +- [lookupUser](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L39) - (required) - provides mapping of username to user ID which is used by other methods +- [authenticateUser](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L51) (optional) - implementation is required in case any Web Flow operation needs to authenticate the user using a username/password +- [fetchUserDetail](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L62) (required) - provides information about the user (user ID and name) for the OAuth 2.0 protocol +- [decorateFormData](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L73) (optional) - implementation is required in case any Web Flow operation form data needs to be updated after authentication (e.g. add information about user bank accounts) +- [formDataChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L83) (optional) - implementation is required in case the client backends need to be notified about user input during an operation +- [operationChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L93) (optional) - implementation is required in case the client backends need to be notified about operation status changes +- [createAuthorizationSms](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L105) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization +- [generateAuthorizationCode](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L115) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization +- [generateSmsText](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L127) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization +- [sendAuthorizationSms](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L138) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization +- [verifyAuthorizationSms](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L151) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization +- [verifyAuthorizationSmsAndPassword](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L166) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization and password +- [initConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L177) (optional) - implementation is required in case the OAuth 2.0 consent step is enabled - [createConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L139) (optional) - implementation is required in case the OAuth 2.0 consent step is enabled - [validateConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L153) (optional) - implementation is required in case the OAuth 2.0 consent step is enabled - [saveConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L165) (optional) - implementation is required in case the OAuth 2.0 consent step is enabled diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java index fc51c348..cb8b61b0 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java @@ -29,7 +29,7 @@ public interface DataAdapter { /** - * Lookup user account. + * Lookup user account - map username to user ID. * @param username Username which user uses for authentication. * @param organizationId Organization ID for this request. * @param operationContext Operation context. @@ -93,7 +93,7 @@ public interface DataAdapter { void operationChangedNotification(String userId, String organizationId, OperationChange operationChange, OperationContext operationContext) throws DataAdapterRemoteException; /** - * Create authorization SMS. + * Create authorization SMS message and send it. * @param userId User ID. * @param organizationId Organization ID. * @param operationContext Operation context. @@ -115,7 +115,7 @@ public interface DataAdapter { AuthorizationCode generateAuthorizationCode(String userId, String organizationId, OperationContext operationContext) throws InvalidOperationContextException; /** - * Generate text for SMS authorization. + * Generate text for SMS authorization message. * @param userId User ID. * @param organizationId Organization ID. * @param operationContext Operation context. diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index 4c84f03f..18133256 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -117,7 +117,7 @@ public DecorateOperationFormDataResponse decorateFormData(String userId, String // In case the bank account selection is disabled, return an empty list. if ((!"authorize_payment".equals(operationName) && !"authorize_payment_sca".equals(operationName))) { - // return empty list for operations other than authorize_payment or authorize_payment_sca + // return empty list for operations other than authorize_payment and authorize_payment_sca return new DecorateOperationFormDataResponse(formData); } @@ -331,6 +331,7 @@ public InitConsentFormResponse initConsentForm(String userId, String organizatio @Override public CreateConsentFormResponse createConsentForm(String userId, String organizationId, OperationContext operationContext, String lang) throws DataAdapterRemoteException, InvalidOperationContextException { + // Generate response with consent text and options based on requested language. if ("login".equals(operationContext.getName()) || "login_sca".equals(operationContext.getName())) { // Create default consent CreateConsentFormResponse response = new CreateConsentFormResponse(); @@ -387,6 +388,7 @@ public CreateConsentFormResponse createConsentForm(String userId, String organiz @Override public ValidateConsentFormResponse validateConsentForm(String userId, String organizationId, OperationContext operationContext, String lang, List options) throws DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException { + // Validate consent form options and return response with result of validation and optional error messages. ValidateConsentFormResponse response = new ValidateConsentFormResponse(); if (options == null || options.isEmpty()) { throw new InvalidConsentDataException("Missing options for consent"); @@ -471,6 +473,7 @@ public ValidateConsentFormResponse validateConsentForm(String userId, String org @Override public SaveConsentFormResponse saveConsentForm(String userId, String organizationId, OperationContext operationContext, List options) throws DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException { + // Save consent form options selected by the user. The sample implementation only logs the selected options. logger.info("Saving consent form for user: {}, operation ID: {}", userId, operationContext.getId()); for (ConsentOption option: options) { logger.info("Option {}: {}", option.getId(), option.getValue()); diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/SmsAuthorizationRepository.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/SmsAuthorizationRepository.java index e9ba5076..1506b249 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/SmsAuthorizationRepository.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/SmsAuthorizationRepository.java @@ -17,7 +17,7 @@ import io.getlime.security.powerauth.app.dataadapter.repository.model.entity.SmsAuthorizationEntity; import org.springframework.data.repository.CrudRepository; -import org.springframework.stereotype.Component; +import org.springframework.stereotype.Repository; /** @@ -25,7 +25,7 @@ * * @author Roman Strobl, roman.strobl@wultra.com */ -@Component +@Repository public interface SmsAuthorizationRepository extends CrudRepository { } From 915fc27c9639ff31aebae071e4f6aa6d6c36c8c0 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Wed, 17 Jul 2019 15:56:00 +0200 Subject: [PATCH 46/79] Code cleanup --- .../powerauth/app/dataadapter/api/DataAdapter.java | 1 + .../dataadapter/controller/FormDataChangeController.java | 2 +- .../dataadapter/controller/OperationChangeController.java | 2 +- .../dataadapter/controller/SmsAuthorizationController.java | 6 +----- .../app/dataadapter/service/DataAdapterI18NService.java | 7 ------- .../app/dataadapter/service/SmsPersistenceService.java | 4 ++-- 6 files changed, 6 insertions(+), 16 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java index cb8b61b0..5c8205d5 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java @@ -33,6 +33,7 @@ public interface DataAdapter { * @param username Username which user uses for authentication. * @param organizationId Organization ID for this request. * @param operationContext Operation context. + * @return Detail about the user. * @throws DataAdapterRemoteException Thrown when remote communication fails. * @throws UserNotFoundException Thrown when user does not exist. */ diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/FormDataChangeController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/FormDataChangeController.java index 436169c2..c6dd7e2e 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/FormDataChangeController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/FormDataChangeController.java @@ -42,7 +42,7 @@ public class FormDataChangeController { private static final Logger logger = LoggerFactory.getLogger(FormDataChangeController.class); - private DataAdapter dataAdapter; + private final DataAdapter dataAdapter; /** * Controller constructor. diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/OperationChangeController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/OperationChangeController.java index d0c27397..01112245 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/OperationChangeController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/OperationChangeController.java @@ -38,7 +38,7 @@ public class OperationChangeController { private static final Logger logger = LoggerFactory.getLogger(OperationChangeController.class); - private DataAdapter dataAdapter; + private final DataAdapter dataAdapter; /** * Controller constructor. diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java index d6625be9..978e6697 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java @@ -21,7 +21,6 @@ import io.getlime.security.powerauth.app.dataadapter.exception.DataAdapterRemoteException; import io.getlime.security.powerauth.app.dataadapter.exception.InvalidOperationContextException; import io.getlime.security.powerauth.app.dataadapter.impl.validation.CreateSmsAuthorizationRequestValidator; -import io.getlime.security.powerauth.app.dataadapter.service.SmsPersistenceService; import io.getlime.security.powerauth.lib.dataadapter.model.entity.AuthenticationContext; import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; import io.getlime.security.powerauth.lib.dataadapter.model.request.CreateSmsAuthorizationRequest; @@ -49,19 +48,16 @@ public class SmsAuthorizationController { private static final Logger logger = LoggerFactory.getLogger(SmsAuthorizationController.class); - private final SmsPersistenceService smsPersistenceService; private final CreateSmsAuthorizationRequestValidator requestValidator; private final DataAdapter dataAdapter; /** * Controller constructor. - * @param smsPersistenceService SMS persistence service. * @param requestValidator Validator for SMS requests. * @param dataAdapter Data adapter. */ @Autowired - public SmsAuthorizationController(SmsPersistenceService smsPersistenceService, CreateSmsAuthorizationRequestValidator requestValidator, DataAdapter dataAdapter) { - this.smsPersistenceService = smsPersistenceService; + public SmsAuthorizationController(CreateSmsAuthorizationRequestValidator requestValidator, DataAdapter dataAdapter) { this.requestValidator = requestValidator; this.dataAdapter = dataAdapter; } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/DataAdapterI18NService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/DataAdapterI18NService.java index 3699643e..8992d8d9 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/DataAdapterI18NService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/DataAdapterI18NService.java @@ -28,13 +28,6 @@ @Service public class DataAdapterI18NService { - - /** - * Data adapter I18N service constructor. - */ - public DataAdapterI18NService() { - } - /** * Get message source with i18n data. * diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SmsPersistenceService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SmsPersistenceService.java index 80cfd001..b3aca997 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SmsPersistenceService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SmsPersistenceService.java @@ -61,10 +61,9 @@ public SmsPersistenceService(SmsAuthorizationRepository smsAuthorizationReposito * @param authorizationCode Authorization code for SMS message. * @param messageText Localized SMS message text. * @return Created entity with SMS message details. - * @throws InvalidOperationContextException In case operation context is invalid. */ public SmsAuthorizationEntity createAuthorizationSms(String userId, String organizationId, String messageId, OperationContext operationContext, - AuthorizationCode authorizationCode, String messageText) throws InvalidOperationContextException { + AuthorizationCode authorizationCode, String messageText) { SmsAuthorizationEntity smsEntity = new SmsAuthorizationEntity(); smsEntity.setMessageId(messageId); @@ -92,6 +91,7 @@ public SmsAuthorizationEntity createAuthorizationSms(String userId, String organ * @param messageId Message ID. * @param authorizationCode Authorization code. * @param allowMultipleVerifications Whether authorization code can be verified multiple times. + * @return Result of SMS verification. */ public VerifySmsAuthorizationResponse verifyAuthorizationSms(String messageId, String authorizationCode, boolean allowMultipleVerifications) { Optional smsEntityOptional = smsAuthorizationRepository.findById(messageId); From 47a67d8aa6912f48d8e8dc092a6f9bee9c5f5f6d Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Wed, 17 Jul 2019 16:51:18 +0200 Subject: [PATCH 47/79] Fix long lines in logging --- .../controller/AuthenticationController.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java index e80fef25..90b19800 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java @@ -80,13 +80,16 @@ private void initBinder(WebDataBinder binder) { */ @RequestMapping(value = "/lookup", method = RequestMethod.POST) public ObjectResponse lookupUser(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException, UserNotFoundException { - logger.info("Received user lookup request, username: {}, organization ID: {}, operation ID: {}", request.getRequestObject().getUsername(), request.getRequestObject().getOrganizationId(), request.getRequestObject().getOperationContext().getId()); + logger.info("Received user lookup request, username: {}, organization ID: {}, operation ID: {}", + request.getRequestObject().getUsername(), request.getRequestObject().getOrganizationId(), + request.getRequestObject().getOperationContext().getId()); UserLookupRequest lookupRequest = request.getRequestObject(); String username = lookupRequest.getUsername(); String organizationId = lookupRequest.getOrganizationId(); OperationContext operationContext = lookupRequest.getOperationContext(); UserDetailResponse response = dataAdapter.lookupUser(username, organizationId, operationContext); - logger.info("The user lookup request succeeded, user ID: {}, organization ID: {}, operation ID: {}", response.getId(), response.getOrganizationId(), request.getRequestObject().getOperationContext().getId()); + logger.info("The user lookup request succeeded, user ID: {}, organization ID: {}, operation ID: {}", + response.getId(), response.getOrganizationId(), request.getRequestObject().getOperationContext().getId()); return new ObjectResponse<>(response); } @@ -99,7 +102,9 @@ public ObjectResponse lookupUser(@Valid @RequestBody ObjectR */ @RequestMapping(value = "/authenticate", method = RequestMethod.POST) public ObjectResponse authenticate(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException { - logger.info("Received authenticate request, user ID: {}, organization ID: {}, operation ID: {}", request.getRequestObject().getUserId(), request.getRequestObject().getOrganizationId(), request.getRequestObject().getOperationContext().getId()); + logger.info("Received authenticate request, user ID: {}, organization ID: {}, operation ID: {}", + request.getRequestObject().getUserId(), request.getRequestObject().getOrganizationId(), + request.getRequestObject().getOperationContext().getId()); UserAuthenticationRequest authenticationRequest = request.getRequestObject(); String userId = authenticationRequest.getUserId(); String password = authenticationRequest.getPassword(); @@ -107,7 +112,8 @@ public ObjectResponse authenticate(@Valid @RequestBo String organizationId = authenticationRequest.getOrganizationId(); OperationContext operationContext = authenticationRequest.getOperationContext(); UserAuthenticationResponse response = dataAdapter.authenticateUser(userId, password, authenticationContext, organizationId, operationContext); - logger.info("The authenticate request succeeded, user ID: {}, organization ID: {}, operation ID: {}", userId, organizationId, request.getRequestObject().getOperationContext().getId()); + logger.info("The authenticate request succeeded, user ID: {}, organization ID: {}, operation ID: {}", userId, + organizationId, request.getRequestObject().getOperationContext().getId()); return new ObjectResponse<>(response); } From 52e8780b380d0208040005b4ae088968f16ddd83 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Wed, 17 Jul 2019 20:44:54 +0200 Subject: [PATCH 48/79] Fix #94: Remove ext-resources folder --- ext-resources/css/base.css | 312 ---------------- ext-resources/css/bootstrap.min.css | 6 - ext-resources/css/customization.css | 1 - ext-resources/css/react-select.css | 344 ------------------ .../fonts/glyphicons-halflings-regular.eot | Bin 20127 -> 0 bytes .../fonts/glyphicons-halflings-regular.svg | 288 --------------- .../fonts/glyphicons-halflings-regular.ttf | Bin 45404 -> 0 bytes .../fonts/glyphicons-halflings-regular.woff | Bin 23424 -> 0 bytes .../fonts/glyphicons-halflings-regular.woff2 | Bin 18028 -> 0 bytes ext-resources/images/image-error.png | Bin 3890 -> 0 bytes ext-resources/images/image-information.png | Bin 5103 -> 0 bytes ext-resources/images/logo.png | Bin 14332 -> 0 bytes ext-resources/images/token.png | Bin 3915 -> 0 bytes ext-resources/messages_cs.properties | 119 ------ ext-resources/messages_en.properties | 119 ------ 15 files changed, 1189 deletions(-) delete mode 100644 ext-resources/css/base.css delete mode 100644 ext-resources/css/bootstrap.min.css delete mode 100644 ext-resources/css/customization.css delete mode 100644 ext-resources/css/react-select.css delete mode 100644 ext-resources/fonts/glyphicons-halflings-regular.eot delete mode 100644 ext-resources/fonts/glyphicons-halflings-regular.svg delete mode 100644 ext-resources/fonts/glyphicons-halflings-regular.ttf delete mode 100644 ext-resources/fonts/glyphicons-halflings-regular.woff delete mode 100644 ext-resources/fonts/glyphicons-halflings-regular.woff2 delete mode 100644 ext-resources/images/image-error.png delete mode 100644 ext-resources/images/image-information.png delete mode 100644 ext-resources/images/logo.png delete mode 100644 ext-resources/images/token.png delete mode 100644 ext-resources/messages_cs.properties delete mode 100644 ext-resources/messages_en.properties diff --git a/ext-resources/css/base.css b/ext-resources/css/base.css deleted file mode 100644 index 58f5de72..00000000 --- a/ext-resources/css/base.css +++ /dev/null @@ -1,312 +0,0 @@ -body { - font-family: "Source Sans Pro", "Helvetica Neue", "Helvetica", sans-serif; - color: #777777; - font-size: 14pt; -} - -.background { - /* background: url('../images/background.png') 0 0 repeat-x;*/ - position: fixed; - top: 0; - left: 0; - z-index: -1; - width: 100%; - height: 100%; -} - -a { - color: #7FC000; - text-decoration: none; -} - -a:hover, a:active, a:focus { - color: #4c7400 !important; - text-decoration: none; -} - -.message-information { - color: #777777; - word-wrap: break-word; -} - -.message-success { - color: #4BA819; - word-wrap: break-word; -} - -.network-error { - width: 200px; - height: auto; - position: absolute; - left: 50%; - margin-left: -100px; - background-color: #383838; - color: #F0F0F0; - font-size: 20px; - padding: 10px; - text-align: center; - border-radius: 5px; - -webkit-box-shadow: 0px 0 24px -1px rgba(150, 150, 150, 1); - -moz-box-shadow: 0px 0px 24px -1px rgba(150, 150, 150, 1); - box-shadow: 0px 0px 24px -1px rgba(150, 150, 150, 1); -} - -.message-error { - color: #C0007F; - word-wrap: break-word; -} - -.font-small { - font-size: 12pt; -} - -.font-tiny { - font-size: 10pt; -} - -#logo { - margin-bottom: 40px; - height: 60px; - background-image: url("../images/logo.png"); - background-position: 50% 50%; - background-size: contain; - background-repeat: no-repeat; -} - -#page-wrap { - margin: 80px auto 40px auto; - max-width: 980px; -} - -#react { - margin-left: 10px; - margin-right: 10px; -} - -#home { - min-height: 400px; -} - -.content-wrap { - margin: 0 auto; - width: 100%; -} - -.btn-success { - background-color: #7FC000; - border: none; -} - -.btn-success:hover, .btn-success:active, .btn-success:focus { - background-color: #4c7400 !important; - color: white !important; - border: none; -} - -.image { - margin: 40px auto; - background-position: 50% 50%; - background-size: contain; - background-repeat: no-repeat; -} - -.image.mtoken { - background-image: url("../images/token.png"); - width: 120px; - height: 120px; - margin: 20px auto; -} - -.image-result { - margin: 40px auto; - width: 80px; - height: 80px; - background-position: 50% 50%; - background-size: contain; - background-repeat: no-repeat; -} - -.image-result.error { - background-image: url("../images/image-error.png"); -} - -.image-result.success { - background-image: url("../images/image-information.png"); -} - -#lang { - position: absolute; - top: 10px; - right: 10px; -} - -#operation .panel-body { - padding: 30px; -} - -#operation .operation-approve { - text-align: center; -} - -#operation .operation-approve h3 { - color: #7FC000; - font-size: 18pt; - margin-top: 0; - margin-bottom: 10px; -} - -#operation .operation-approve p { - color: #777777; - font-size: 14pt; -} - -#operation .key { - color: #777777; - word-wrap: break-word; -} - -#operation .value { - color: #333333; - word-wrap: break-word; -} - -#operation .col-xs-6.key { - text-align: left; -} - -#operation .col-xs-6.value { - text-align: right; -} - -#operation .col-xs-12 .key { - text-align: left; -} - -#operation .col-xs-12 { - text-align: left; -} - -#operation .col-xs-12 .value { - text-align: left; -} - -#operation .amount { - font-size: 16pt; -} - -#operation .heading { - font-size: 16pt; - color: #7FC000; -} - -#operation .attribute { - margin-top: 10px; - margin-bottom: 10px; -} - -#operation .auth-actions { - margin-top: 20px; -} - -#operation .buttons { - margin-top: 20px; -} - -#operation .btn { - width: 100%; -} - -/* Base styling */ - -.tint { - color: #7FC000; -} - -.strong { - font-weight: bold; -} - -/* React Select Fix */ - -.Select-input { - height: 68px; -} - -.Select-placeholder { - line-height: 68px -} - -/* LOGIN */ - -#login .panel-body { - padding: 30px; -} - -#login .panel-body .title { - color: #7FC000; -} - -#login .buttons { - margin-top: 40px; -} - -#login .btn { - width: 100%; -} - -.panel { - border-radius: 20px; - box-shadow: 0 0 12px rgba(0, 0, 0, 0.20); -} - -.title { - font-size: 16pt; - margin: 10px 10px 20px 10px; -} - -.alert { - font-size: 12pt; - text-align: left; - width: 100%; -} - -.alert-form { - margin: 0 0 15px 0; - padding: 10px 15px 10px 15px; -} - -.alert-field { - margin: 0; - padding: 2px 15px 2px 15px; -} - -.party-info-wrapper { - background-color: #FAFAFA; - border-radius: 5px; - padding: 10px; -} - -.party-info-wrapper * { - margin: 0; -} - -.party-info-logo-wrapper { - padding: 10px; -} - -.party-info-logo { - width: 100%; -} - -.party-info-name { - font-size: 16pt; - font-weight: bold; -} - -.party-info-description { - font-size: 14pt; -} - -.party-info-link { - font-size: 12pt; -} \ No newline at end of file diff --git a/ext-resources/css/bootstrap.min.css b/ext-resources/css/bootstrap.min.css deleted file mode 100644 index 4cf729e4..00000000 --- a/ext-resources/css/bootstrap.min.css +++ /dev/null @@ -1,6 +0,0 @@ -/*! - * Bootstrap v3.3.6 (http://getbootstrap.com) - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;filter:alpha(opacity=0);opacity:0;line-break:auto}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);line-break:auto}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} -/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/ext-resources/css/customization.css b/ext-resources/css/customization.css deleted file mode 100644 index 25c3b006..00000000 --- a/ext-resources/css/customization.css +++ /dev/null @@ -1 +0,0 @@ -/* Add customized CSS styles into this file. This CSS file is loaded after all other CSS files. */ \ No newline at end of file diff --git a/ext-resources/css/react-select.css b/ext-resources/css/react-select.css deleted file mode 100644 index c28af38f..00000000 --- a/ext-resources/css/react-select.css +++ /dev/null @@ -1,344 +0,0 @@ -.Select, .Select-control { - position: relative -} - -.Select, .Select div, .Select input, .Select span { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box -} - -.Select.is-disabled > .Select-control { - background-color: #f6f6f6 -} - -.Select.is-disabled .Select-arrow-zone { - cursor: default; - pointer-events: none -} - -.Select-control { - background-color: #fff; - border-radius: 4px; - border: 1px solid #ccc; - color: #333; - cursor: default; - display: table; - height: 36px; - outline: 0; - overflow: hidden; - width: 100% -} - -.is-searchable.is-focused:not(.is-open) > .Select-control, .is-searchable.is-open > .Select-control { - cursor: text -} - -.Select-placeholder, .Select-value { - left: 0; - position: absolute; - top: 0; - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap -} - -.Select-control:hover { - box-shadow: 0 1px 0 rgba(0, 0, 0, .06) -} - -.is-open > .Select-control { - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; - background: #fff; - border-color: #b3b3b3 #ccc #d9d9d9 -} - -.is-open > .Select-control > .Select-arrow { - border-color: transparent transparent #999; - border-width: 0 5px 5px -} - -.is-focused:not(.is-open) > .Select-control { - border-color: #08c #0099e6 #0099e6; - box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1), 0 0 5px -1px rgba(0, 136, 204, .5) -} - -.Select-placeholder { - bottom: 0; - color: #aaa; - line-height: 34px; - padding-left: 10px; - padding-right: 10px; - right: 0 -} - -.has-value > .Select-control > .Select-placeholder { - color: #333 -} - -.Select-value { - color: #aaa; - padding: 8px 52px 8px 10px; - right: -15px -} - -.Select-arrow-zone, .Select-clear-zone, .Select-loading, .Select-loading-zone { - position: relative; - vertical-align: middle -} - -.has-value > .Select-control > .Select-value { - color: #333 -} - -.Select-input { - height: 34px; - padding-left: 10px; - padding-right: 10px; - vertical-align: middle; - visibility: hidden -} - -.Select-input > input { - background: none; - border: 0; - box-shadow: none; - cursor: default; - display: inline-block; - font-family: inherit; - font-size: inherit; - height: 34px; - margin: 0; - outline: 0; - padding: 0; - -webkit-appearance: none -} - -.is-focused .Select-input > input { - cursor: text -} - -.Select-control:not(.is-searchable) > .Select-input { - outline: 0 -} - -.Select-loading-zone { - cursor: pointer; - display: table-cell; - text-align: center; - width: 16px -} - -.Select-loading { - -webkit-animation: Select-animation-spin .4s infinite linear; - -o-animation: Select-animation-spin .4s infinite linear; - animation: Select-animation-spin .4s infinite linear; - width: 16px; - height: 16px; - box-sizing: border-box; - border-radius: 50%; - border: 2px solid #ccc; - border-right-color: #333; - display: inline-block -} - -.Select-clear-zone { - -webkit-animation: Select-animation-fadeIn .2s; - -o-animation: Select-animation-fadeIn .2s; - animation: Select-animation-fadeIn .2s; - color: #999; - cursor: pointer; - display: table-cell; - text-align: center; - width: 17px -} - -.Select-clear-zone:hover { - color: #D0021B -} - -.Select-clear { - display: inline-block; - font-size: 18px; - line-height: 1 -} - -.Select--multi .Select-clear-zone { - width: 17px -} - -.Select-arrow-zone { - cursor: pointer; - display: table-cell; - text-align: center; - width: 25px; - padding-right: 5px -} - -.Select-arrow { - border-color: #999 transparent transparent; - border-style: solid; - border-width: 5px 5px 2.5px; - display: inline-block; - height: 0; - width: 0 -} - -.Select-arrow-zone:hover > .Select-arrow, .is-open .Select-arrow { - border-top-color: #666 -} - -@-webkit-keyframes Select-animation-fadeIn { - from { - opacity: 0 - } - to { - opacity: 1 - } -} - -@keyframes Select-animation-fadeIn { - from { - opacity: 0 - } - to { - opacity: 1 - } -} - -.Select-menu-outer { - border-bottom-right-radius: 4px; - border-bottom-left-radius: 4px; - background-color: #fff; - border: 1px solid #ccc; - border-top-color: #e6e6e6; - box-shadow: 0 1px 0 rgba(0, 0, 0, .06); - box-sizing: border-box; - margin-top: -1px; - max-height: 200px; - position: absolute; - top: 100%; - width: 100%; - z-index: 1000; - -webkit-overflow-scrolling: touch -} - -.Select-menu { - max-height: 198px; - overflow-y: auto -} - -.Select-option { - box-sizing: border-box; - color: #666; - cursor: pointer; - display: block; - padding: 8px 10px -} - -.Select-option:last-child { - border-bottom-right-radius: 4px; - border-bottom-left-radius: 4px -} - -.Select-option.is-focused { - background-color: #f2f9fc; - color: #333 -} - -.Select-option.is-disabled { - color: #ccc; - cursor: not-allowed -} - -.Select-noresults, .Select-search-prompt, .Select-searching { - box-sizing: border-box; - color: #999; - cursor: default; - display: block; - padding: 8px 10px -} - -.Select--multi .Select-input { - vertical-align: middle; - margin-left: 0; - padding: 0 -} - -.Select--multi.has-value .Select-input, .Select-item { - margin-left: 5px -} - -.Select-item { - background-color: #f2f9fc; - border-radius: 2px; - border: 1px solid #c9e6f2; - color: #08c; - display: inline-block; - font-size: .9em; - margin-top: 5px; - vertical-align: top -} - -.Select-item-icon, .Select-item-label { - display: inline-block; - vertical-align: middle -} - -.Select-item-label { - border-bottom-right-radius: 2px; - border-top-right-radius: 2px; - cursor: default; - padding: 2px 5px -} - -.Select-item-label .Select-item-label__a { - color: #08c; - cursor: pointer -} - -.Select-item-icon { - cursor: pointer; - border-bottom-left-radius: 2px; - border-top-left-radius: 2px; - border-right: 1px solid #c9e6f2; - padding: 1px 5px 3px -} - -.Select-item-icon:focus, .Select-item-icon:hover { - background-color: #ddeff7; - color: #0077b3 -} - -.Select-item-icon:active { - background-color: #c9e6f2 -} - -.Select--multi.is-disabled .Select-item { - background-color: #f2f2f2; - border: 1px solid #d9d9d9; - color: #888 -} - -.Select--multi.is-disabled .Select-item-icon { - cursor: not-allowed; - border-right: 1px solid #d9d9d9 -} - -.Select--multi.is-disabled .Select-item-icon:active, .Select--multi.is-disabled .Select-item-icon:focus, .Select--multi.is-disabled .Select-item-icon:hover { - background-color: #f2f2f2 -} - -@keyframes Select-animation-spin { - to { - transform: rotate(1turn) - } -} - -@-webkit-keyframes Select-animation-spin { - to { - -webkit-transform: rotate(1turn) - } -} \ No newline at end of file diff --git a/ext-resources/fonts/glyphicons-halflings-regular.eot b/ext-resources/fonts/glyphicons-halflings-regular.eot deleted file mode 100644 index b93a4953fff68df523aa7656497ee339d6026d64..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20127 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/ext-resources/fonts/glyphicons-halflings-regular.ttf b/ext-resources/fonts/glyphicons-halflings-regular.ttf deleted file mode 100644 index 1413fc609ab6f21774de0cb7e01360095584f65b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45404 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 G`u_lkUM?>H diff --git a/ext-resources/fonts/glyphicons-halflings-regular.woff2 b/ext-resources/fonts/glyphicons-halflings-regular.woff2 deleted file mode 100644 index 64539b54c3751a6d9adb44c8e3a45ba5a73b77f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18028 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 diff --git a/ext-resources/images/image-error.png b/ext-resources/images/image-error.png deleted file mode 100644 index 19f3a7832132da8806cfdade9748cf90925b0a5c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3890 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 diff --git a/ext-resources/messages_cs.properties b/ext-resources/messages_cs.properties deleted file mode 100644 index fcf7d364..00000000 --- a/ext-resources/messages_cs.properties +++ /dev/null @@ -1,119 +0,0 @@ -error.authPage=Nelze zobrazit autentizaฤnรญ strรกnku. -error.sessionExpired=Vaลกe relace byla ukonฤena z dลฏvodu delลกรญ neฤinnosti. -error.sessionTerminated=Relace byla ukonฤena z dลฏvodu delลกรญ neฤinnosti. -error.invalidRequest=Doลกlo k chybฤ› pล™i vykonรกvรกnรญ poลพadavku. -error.noAuthMethod=Nebyla nalezena ลพรกdnรก vhodnรก autentizaฤnรญ metoda. -error.remote=Nastala chyba pล™i komunikaci se vzdรกlenรฝm systรฉmem. -error.unknown=Nastala neznรกmรก chyba. -login.signIn=Pล™ihlรกsit -login.cancel=Zruลกit -login.loginNumber=Pล™ihlaลกovacรญ ฤรญslo -login.password=Heslo -login.pleaseLogIn=Pล™ihlaลกte se prosรญm -login.authenticationFailed=Pล™ihlรกลกenรญ se nezdaล™ilo. -login.authenticationBlocked=Byl pล™ekroฤen maximรกlnรญ poฤet pokusลฏ pro pล™ihlรกลกenรญ. Vรกลก รบฤet byl proto doฤasnฤ› zablokovรกn. -login.username.empty=Vyplลˆte vaลกe pล™ihlaลกovacรญ ฤรญslo. -login.password.empty=Vyplลˆte heslo. -login.username.empty\ login.password.empty=Vyplลˆte pล™ihlaลกovacรญ ฤรญslo a heslo. -login.username.long=Pล™ihlaลกovacรญ ฤรญslo je pล™รญliลก dlouhรฉ. -login.password.long=Heslo je pล™รญliลก dlouhรฉ. -login.username.long\ login.password.long=Pล™ihlaลกovacรญ ฤรญslo i heslo jsou pล™รญliลก dlouhรก. -login.password.empty\ login.username.long=Pล™ihlaลกovacรญ ฤรญslo je pล™รญliลก dlouhรฉ. Vyplลˆte prosรญm heslo. -login.username.empty\ login.password.long=Vyplลˆte pล™ihlaลกovacรญ ฤรญslo. Heslo je pล™รญliลก dlouhรฉ. -login.type.unsupported=Nepodporovanรฝ typ autentizace. -operationContext.missing=Nenรญ dostupnรฝ kontext operace. -operationConfig.missing=Chybรญ konfigurace operace. -operationData.invalid=Operace obsahuje chybnรก data. -operation.confirm=Potvrdit -operation.cancel=Zruลกit -authentication.success=Operace byla รบspฤ›ลกnฤ› potvrzena. -authentication.fail=Nepodaล™ilo se potvrdit operaci. -authentication.attemptsRemaining=Zbรฝvรก pokusลฏ: -authentication.maxAttemptsExceeded=Byl pล™ekroฤen maximรกlnรญ poฤet pokusลฏ pro pล™ihlรกลกenรญ. -authentication.authenticationBlocked=Byl pล™ekroฤen maximรกlnรญ poฤet pokusลฏ pro pล™ihlรกลกenรญ. Vรกลก รบฤet byl proto doฤasnฤ› zablokovรกn. -authorization.success=รšspฤ›ลกnฤ› jsme vรกs ovฤ›ล™ili. -authorization.fail=Nepodaล™ilo se ovฤ›ล™it uลพivatele. -message.redirect=Za okamลพik probฤ›hne pล™esmฤ›rovรกnรญ. -message.networkError=Nenรญ dostupnรฉ pล™ipojenรญ k Internetu. -message.token.confirm=Potvrฤte prosรญm operaci na vaลกem mobilnรญm zaล™รญzenรญ. -message.token.offline=Nemรกte na mobilnรญm zaล™รญzenรญ data a nepล™iลกla vรกm notifikace? Nevadรญ. -message.token.offline.link=Potvrฤte platbu ovฤ›ล™enรญm pล™es QR kรณd -message.sms.confirm=Potvrฤte operaci pomocรญ SMS klรญฤe. -method.usernamePassword=Pล™ihlรกลกenรญ -method.showOperationDetail=Detail operace -method.powerauthToken=Mobilnรญ aplikace -method.smsKey=Autorizaฤnรญ SMS kรณd -operation.timeout=Vyprลกel ฤasovรฝ limit pro potvrzenรญ operace. -operation.canceled=Operace byla zruลกena uลพivatelem. -operation.notAvailable=Operace jiลพ nenรญ dostupnรก. -operation.alreadyFailed=Operace jiลพ selhala. -operation.noMethod=Operaci nelze ovฤ›ล™it, protoลพe nenรญ k dispozici ลพรกdnรก vhodnรก autentizaฤnรญ metoda. -operation.confirmationTextChoice=Vyberte zpลฏsob potvrzenรญ -operation.confirmationText=Potvrฤte operaci: -operation.methodSelectionOr=nebo -operation.invalidChosenMethod=Vybranรก autentizaฤnรญ metoda nenรญ platnรก. -operation.missingHistory=Nenรญ dostupnรก historie operace. -operation.methodNotAvailable=Autentizaฤnรญ metoda nenรญ dostupnรก. -operation.interrupted=Tato operace byla pล™eruลกena, protoลพe doลกlo k vytvoล™enรญ novฤ›jลกรญ operace. -canceled.unknown=Operace byla zruลกena z neznรกmรฉho dลฏvodu. -canceled.incorrectData=Operace byla zruลกena, protoลพe obsahovala nesprรกvnรก data. -canceled.unexpectedOperation=Operace byla zruลกena, protoลพe nebyla oฤekรกvรกna. -smsAuthorization.invalidMessage=Nalezena chybnรก zprรกva pล™i ovฤ›ล™ovรกnรญ operace pomocรญ SMS. -smsAuthorization.invalidCode=Nalezen chybnรฝ autentizaฤnรญ kรณd pล™i ovฤ›ล™ovรกnรญ operace pomocรญ SMS. -smsAuthorization.expired=Zprรกva pro ovฤ›ล™enรญ pomocรญ SMS jiลพ nenรญ aktuรกlnรญ. -smsAuthorization.alreadyVerified=Zprรกva pro ovฤ›ล™enรญ pomocรญ SMS jiลพ byla pouลพita. -smsAuthorization.maxAttemptsExceeded=Byl pล™ekroฤen maximรกlnรญ poฤet pokusลฏ o ovฤ›ล™enรญ pomocรญ SMS zprรกvy. -smsAuthorization.failed=Ovฤ›ล™enรญ pomocรญ SMS klรญฤe se nezdaล™ilo. -smsAuthorization.userId.empty=Ovฤ›ล™enรญ pomocรญ SMS klรญฤe selhalo z dลฏvodu chybฤ›jรญcรญ hodnoty userId. -smsAuthorization.operationName.empty=Ovฤ›ล™enรญ pomocรญ SMS klรญฤe selhalo z dลฏvodu chybฤ›jรญcรญho nรกzvu operace. -smsAuthorization.userId.long=Ovฤ›ล™enรญ pomocรญ SMS klรญฤe selhalo z dลฏvodu pล™รญliลก dlouhรฉ hodnoty userId. -smsAuthorization.operationName.long=Ovฤ›ล™enรญ pomocรญ SMS klรญฤe selhalo z dลฏvodu pล™รญliลก dlouhรฉho nรกzvu operace. -smsAuthorization.amount.empty=Ovฤ›ล™enรญ pomocรญ SMS klรญฤe selhalo z dลฏvodu chybฤ›jรญcรญ ฤรกstky. -smsAuthorization.amount.invalid=Ovฤ›ล™enรญ pomocรญ SMS klรญฤe selhalo z dลฏvodu ลกpatnรฉho formรกtu ฤรกstky. -smsAuthorization.currency.empty=Ovฤ›ล™enรญ pomocรญ SMS klรญฤe selhalo z dลฏvodu chybฤ›jรญcรญ mฤ›ny. -smsAuthorization.account.empty=Ovฤ›ล™enรญ pomocรญ SMS klรญฤe selhalo z dลฏvodu chybฤ›jรญcรญho รบฤtu. -smsAuthorization.authCodeText=Zadejte autorizaฤnรญ kรณd,\n kterรฝ jsme vรกm poslali v SMS zprรกvฤ›. -operationReview.bankAccountsMissing=Operace selhala, protoลพe chybรญ informace o bankovnรญch รบฤtech. -operationReview.bankAccount.balance=Zลฏstatek: -offlineMode.instructions=V mobilnรญ aplikaci\nzvolte menu 'Operace k potvrzenรญ', nรกslednฤ› 'Ovฤ›ล™enรญ pล™es QR kรณd' a naฤtฤ›te tento QR kรณd: -offlineMode.authCodeText=Opiลกte ฤรญselnรฝ kรณd z mobilnรญ aplikace -offlineMode.invalidAuthCode=Chybnรฝ ovฤ›ล™ovacรญ kรณd. -offlineMode.device=Mobilnรญ zaล™รญzenรญ pro autorizaci -offlineMode.invalidData=Chybnรก data pro podpis. -offlineMode.noActivation=Nebylo nalezenรฉ ลพรกdnรฉ odpovรญdajรญcรญ mobilnรญ zaล™รญzenรญ. -offlineMode.invalidActivation=Mobilnรญho zaล™รญzenรญ je chybnฤ› nastavenรฉ. -offlineMode.activationNotActive=Mobilnรญ zaล™รญzenรญ nenรญ nastaveno pro potvrzovรกnรญ operacรญ. -offlineMode.disabled=Offline mรณd nenรญ dostupnรฝ. -pushMessage.fail=Nepodaล™ilo se odeslat push notifikaci. -pushMessage.noActivation=Nepodaล™ilo se odeslat push notifikaci, protoลพe nebyla nalezena aktivace mobilnรญho zaล™รญzenรญ. -pushMessage.activationNotActive=Nepodaล™ilo se odeslat push notifikaci, protoลพe mobilnรญ zaล™รญzenรญ nenรญ aktivovรกno. -pushMessage.noApplication=Nepodaล™ilo se odeslat push notifikaci kvลฏli chybnรฉmu nastavenรญ aplikace. -push.confirmOperation=Potvrฤte operaci -push.data=Data: {0} - -# Localize operations here -login.title=Pล™ihlรกลกenรญ -login.greeting=Dobrรฝ den,\npล™ihlaลกte se, prosรญm. -login.summary=Potvrฤte prosรญm pล™ihlรกลกenรญ uลพivatele. -operation.title=Potvrzenรญ platby -operation.greeting=Dobrรฝ den,\nprosรญme o potvrzenรญ nรกsledujรญcรญ platby: -operation.summary=Potvrฤte platbu {operation.amount} {operation.currency} na รบฤet {operation.account}. -operation.amount=ฤŒรกstka -operation.bankAccountChoice=Z vaลกeho รบฤtu -operation.account=Na รบฤet -operation.currency=Mฤ›na -operation.note=Poznรกmka -operation.dueDate=Datum splatnosti -operation.partyInfo=Aplikace - -# Browser support -browser.unsupported=Vรกลก prohlรญลพeฤ nenรญ podporovรกn. Prosรญm pouลพijte novฤ›jลกรญ verzi prohlรญลพeฤe. - -# Currencies -currency.pattern=###0.00 -currency.USD.name=USD -currency.EUR.name=EUR -currency.CZK.name=Kฤ - -# Party info -partyInfo.websiteLink=Vรญce informacรญ diff --git a/ext-resources/messages_en.properties b/ext-resources/messages_en.properties deleted file mode 100644 index d047a258..00000000 --- a/ext-resources/messages_en.properties +++ /dev/null @@ -1,119 +0,0 @@ -error.authPage=Unable to display authentication page. -error.sessionExpired=Your session has expired due to inactivity. -error.sessionTerminated=Session has been terminated due to inactivity. -error.invalidRequest=Invalid request. -error.noAuthMethod=No authentication method is available to serve the request. -error.remote=Error occurred during communication with the remote system. -error.unknown=Unknown error occurred. -login.signIn=Sign In -login.cancel=Cancel -login.loginNumber=Login number -login.password=Password -login.pleaseLogIn=Please sign in -login.authenticationFailed=User authentication failed. -login.authenticationBlocked=The maximum number of authentication attempts has been exceeded. Your account was blocked temporarily. -login.username.empty=Fill in the login number. -login.password.empty=Fill in the password. -login.username.empty\ login.password.empty=Fill in the login number and password. -login.username.long=Supplied login number is too long. -login.password.long=Supplied password is too long. -login.username.long\ login.password.long=Supplied login number and password are too long. -login.password.empty\ login.username.long=Supplied login number is too long. Fill in the password. -login.username.empty\ login.password.long=Fill in the login number. Supplied password is too long. -login.type.unsupported=Unsupported authentication type. -operationContext.missing=Operation context is not available. -operationConfig.missing=Operation is not configured. -operationData.invalid=Operation contains invalid data. -operation.confirm=Confirm -operation.cancel=Cancel -authentication.success=Operation was successfully authorized. -authentication.fail=Authorization failed. -authentication.attemptsRemaining=Remaining attempts: -authentication.maxAttemptsExceeded=The maximum number of authentication attempts has been exceeded. -authentication.authenticationBlocked=The maximum number of authentication attempts has been exceeded. Your account was blocked temporarily. -authorization.success=Authorization succeeded. -authorization.fail=Authorization failed. -message.redirect=You will be redirected back in a moment. -message.networkError=Internet connection is not available. -message.token.confirm=Please confirm the operation on your mobile device. -message.token.offline=No data on the mobile device? No problem. -message.token.offline.link=Confirm the operation via the QR code. -message.sms.confirm=Please authorize the operation using the SMS key. -method.usernamePassword=Login -method.showOperationDetail=Operation Detail -method.powerauthToken=Mobile Application -method.smsKey=SMS Key Authorization -operation.timeout=Operation has timed out. -operation.canceled=Operation was canceled by the user. -operation.notAvailable=Operation is no longer available. -operation.alreadyFailed=Operation has already failed. -operation.confirmationTextChoice=Choose the confirmation method -operation.confirmationText=Confirm the operation: -operation.methodSelectionOr=or -operation.noMethod=Operation cannot be confirmed, because there is no authorization method available. -operation.invalidChosenMethod=Chosen authentication method is not valid. -operation.missingHistory=Operation history is not available. -operation.methodNotAvailable=Authentication method is not available. -operation.interrupted=This operation has been interrupted by a newer operation. -canceled.unknown=Operation has been canceled due to an unknown reason. -canceled.incorrectData=Operation has been canceled because it contained incorrect data. -canceled.unexpectedOperation=Operation has been canceled because it was not expected. -smsAuthorization.invalidMessage=Invalid message sent while authorizing operation using SMS. -smsAuthorization.invalidCode=Invalid authentication code sent while authorizing operation using SMS. -smsAuthorization.expired=SMS authorization message is already expired. -smsAuthorization.alreadyVerified=SMS authorization message has already been used. -smsAuthorization.maxAttemptsExceeded=SMS authorization code has been rejected because maximum number of tries has been exceeded. -smsAuthorization.failed=SMS authorization failed. -smsAuthorization.userId.empty=SMS authorization failed due to missing userId value. -smsAuthorization.operationName.empty=SMS authorization failed due to missing operation name. -smsAuthorization.userId.long=SMS authorization failed due to too long userId value. -smsAuthorization.operationName.long=SMS authorization failed due to too long operation name. -smsAuthorization.amount.empty=SMS authorization failed due to missing amount. -smsAuthorization.amount.invalid=SMS authorization failed due to invalid format of amount. -smsAuthorization.currency.empty=SMS authorization failed due to missing currency. -smsAuthorization.account.empty=SMS authorization failed due to missing account. -smsAuthorization.authCodeText=Enter authorization code we sent you in SMS message. -operationReview.bankAccountsMissing=Operation failed because bank account details are not available. -operationReview.bankAccount.balance=Balance: -offlineMode.instructions=Choose 'Pending operations' menu, 'QR Code Verification' in mobile app and scan this QR code: -offlineMode.authCodeText=Retype numeric code shown in mobile application -offlineMode.invalidAuthCode=Invalid authorization code. -offlineMode.device=Mobile device for authorization -offlineMode.invalidData=Invalid signature data. -offlineMode.noActivation=Mobile device activation is missing. -offlineMode.invalidActivation=Mobile device activation is invalid. -offlineMode.activationNotActive=Mobile device is not activated. -offlineMode.disabled=Offline mode is not available. -pushMessage.fail=Push notification delivery failed. -pushMessage.noActivation=Push notification delivery failed because of missing mobile device activation. -pushMessage.activationNotActive=Push notification delivery failed because mobile device is not activated. -pushMessage.noApplication=Push notification delivery failed because of invalid application configuration. -push.confirmOperation=Confirm operation -push.data=Data: {0} - -# Localize operations here -login.title=Login -login.greeting=Hello,\nplease sign in. -login.summary=Please confirm the login attempt. -operation.title=Confirm Payment -operation.greeting=Hello,\nplease confirm following payment: -operation.summary=Hello, please confirm payment {operation.amount} {operation.currency} to account {operation.account}. -operation.amount=Amount -operation.bankAccountChoice=From Your Account -operation.account=To Account -operation.currency=Currency -operation.note=Note -operation.dueDate=Due Date -operation.partyInfo=Application - -# Browser support -browser.unsupported=Your web browser is not supported. Please switch to a newer version of a web browser. - -# Currencies -currency.pattern=#,##0.00 -currency.USD.name=USD -currency.EUR.name=EUR -currency.CZK.name=CZK - -# Party info -partyInfo.websiteLink=More information From bb9785be31723a00a8c244690518fcb08b7f442e Mon Sep 17 00:00:00 2001 From: snyk-test Date: Sat, 20 Jul 2019 02:04:36 +0000 Subject: [PATCH 49/79] fix: powerauth-data-adapter/pom.xml to reduce vulnerabilities The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JAVA-COMFASTERXMLJACKSONCORE-450207 - https://snyk.io/vuln/SNYK-JAVA-COMFASTERXMLJACKSONCORE-450917 --- powerauth-data-adapter/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/powerauth-data-adapter/pom.xml b/powerauth-data-adapter/pom.xml index bbbd9652..4c27a27b 100644 --- a/powerauth-data-adapter/pom.xml +++ b/powerauth-data-adapter/pom.xml @@ -15,7 +15,7 @@ org.springframework.boot spring-boot-starter-parent 2.1.6.RELEASE - + 2018 @@ -101,7 +101,7 @@ com.fasterxml.jackson.datatype jackson-datatype-joda - 2.9.9 + 2.10.0.pr1 org.bouncycastle From 7851fec14fd782e797ed612f16cf9a9c78228032 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Wed, 24 Jul 2019 18:30:29 +0200 Subject: [PATCH 50/79] Fix #97: Update Data Adapter interface for SMS delivery --- .../app/dataadapter/api/DataAdapter.java | 35 +---- .../SmsAuthorizationController.java | 4 +- .../impl/service/DataAdapterService.java | 116 ++++---------- .../impl/service/SmsDeliveryService.java | 145 ++++++++++++++++++ 4 files changed, 176 insertions(+), 124 deletions(-) create mode 100644 powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/SmsDeliveryService.java diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java index 5c8205d5..d59d4bcc 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java @@ -103,40 +103,7 @@ public interface DataAdapter { * @throws InvalidOperationContextException Thrown when operation context is invalid. * @throws DataAdapterRemoteException Thrown when remote communication fails or SMS message could not be delivered. */ - String createAuthorizationSms(String userId, String organizationId, OperationContext operationContext, String lang) throws InvalidOperationContextException, DataAdapterRemoteException; - - /** - * Generate authorization code for SMS authorization. - * @param userId User ID. - * @param organizationId Organization ID. - * @param operationContext Operation context. - * @return Authorization code. - * @throws InvalidOperationContextException Thrown when operation context is invalid. - */ - AuthorizationCode generateAuthorizationCode(String userId, String organizationId, OperationContext operationContext) throws InvalidOperationContextException; - - /** - * Generate text for SMS authorization message. - * @param userId User ID. - * @param organizationId Organization ID. - * @param operationContext Operation context. - * @param authorizationCode Authorization code. - * @param lang Language for localization. - * @return Generated SMS text with authorization code. - * @throws InvalidOperationContextException Thrown when operation context is invalid. - */ - String generateSmsText(String userId, String organizationId, OperationContext operationContext, AuthorizationCode authorizationCode, String lang) throws InvalidOperationContextException; - - /** - * Send an authorization SMS with generated authorization code. - * @param userId User ID. - * @param organizationId Organization ID. - * @param messageId Message ID. - * @param messageText Text of SMS message. - * @param operationContext Operation context. - * @throws DataAdapterRemoteException Thrown when remote communication fails or SMS message could not be delivered. - */ - void sendAuthorizationSms(String userId, String organizationId, String messageId, String messageText, OperationContext operationContext) throws DataAdapterRemoteException; + CreateSmsAuthorizationResponse createAndSendAuthorizationSms(String userId, String organizationId, OperationContext operationContext, String lang) throws InvalidOperationContextException, DataAdapterRemoteException; /** * Verify authorization code from SMS message. diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java index 978e6697..64897aa3 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java @@ -89,10 +89,8 @@ public ObjectResponse createAuthorizationSms(@Va String organizationId = smsRequest.getOrganizationId(); OperationContext operationContext = smsRequest.getOperationContext(); String lang = smsRequest.getLang(); - String messageId = dataAdapter.createAuthorizationSms(userId, organizationId, operationContext, lang); + CreateSmsAuthorizationResponse response = dataAdapter.createAndSendAuthorizationSms(userId, organizationId, operationContext, lang); - // Create response. - CreateSmsAuthorizationResponse response = new CreateSmsAuthorizationResponse(messageId); logger.info("The createAuthorizationSms request succeeded, operation ID: {}", request.getRequestObject().getOperationContext().getId()); return new ObjectResponse<>(response); } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index 18133256..38c90842 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -4,12 +4,11 @@ import io.getlime.security.powerauth.app.dataadapter.exception.*; import io.getlime.security.powerauth.app.dataadapter.service.DataAdapterI18NService; import io.getlime.security.powerauth.app.dataadapter.service.SmsPersistenceService; -import io.getlime.security.powerauth.crypto.server.util.DataDigest; import io.getlime.security.powerauth.lib.dataadapter.model.entity.*; -import io.getlime.security.powerauth.lib.dataadapter.model.entity.attribute.AmountAttribute; import io.getlime.security.powerauth.lib.dataadapter.model.entity.attribute.FormFieldConfig; import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.PasswordProtectionType; import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.SmsAuthorizationResult; +import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.SmsDeliveryResult; import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.UserAuthenticationResult; import io.getlime.security.powerauth.lib.dataadapter.model.response.*; import org.slf4j.Logger; @@ -21,7 +20,6 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; -import java.util.Locale; import java.util.UUID; /** @@ -35,16 +33,19 @@ public class DataAdapterService implements DataAdapter { private static final Logger logger = LoggerFactory.getLogger(DataAdapterService.class); private static final String BANK_ACCOUNT_CHOICE_ID = "operation.bankAccountChoice"; + private static final String AUTHENTICATION_FAILED = "login.authenticationFailed"; + private static final String SMS_DELIVERY_FAILED = "smsAuthorization.deliveryFailed"; + private static final String SMS_AUTHORIZATION_FAILED = "smsAuthorization.failed"; private final DataAdapterI18NService dataAdapterI18NService; - private final OperationValueExtractionService operationValueExtractionService; private final SmsPersistenceService smsPersistenceService; + private final SmsDeliveryService smsDeliveryService; @Autowired - public DataAdapterService(DataAdapterI18NService dataAdapterI18NService, OperationValueExtractionService operationValueExtractionService, SmsPersistenceService smsPersistenceService) { + public DataAdapterService(DataAdapterI18NService dataAdapterI18NService, SmsPersistenceService smsPersistenceService, SmsDeliveryService smsDeliveryService) { this.dataAdapterI18NService = dataAdapterI18NService; - this.operationValueExtractionService = operationValueExtractionService; this.smsPersistenceService = smsPersistenceService; + this.smsDeliveryService = smsDeliveryService; } @Override @@ -75,12 +76,12 @@ public UserAuthenticationResponse authenticateUser(String userId, String passwor return authResponse; } catch (UserNotFoundException e) { authResponse.setAuthenticationResult(UserAuthenticationResult.VERIFIED_FAILED); - authResponse.setErrorMessage("login.authenticationFailed"); + authResponse.setErrorMessage(AUTHENTICATION_FAILED); return authResponse; } } authResponse.setAuthenticationResult(UserAuthenticationResult.VERIFIED_FAILED); - authResponse.setErrorMessage("login.authenticationFailed"); + authResponse.setErrorMessage(AUTHENTICATION_FAILED); // Set number of remaining attempts for this user ID in case it is available. // authResponse.setRemainingAttempts(5); @@ -194,96 +195,37 @@ public void operationChangedNotification(String userId, String organizationId, O } @Override - public String createAuthorizationSms(String userId, String organizationId, OperationContext operationContext, String lang) throws InvalidOperationContextException, DataAdapterRemoteException { - // messageId is generated as random UUID, it can be overridden to provide a real message identification + public CreateSmsAuthorizationResponse createAndSendAuthorizationSms(String userId, String organizationId, OperationContext operationContext, String lang) throws InvalidOperationContextException, DataAdapterRemoteException { + CreateSmsAuthorizationResponse response = new CreateSmsAuthorizationResponse(); + // MessageId is generated as random UUID, it can be overridden to provide a real message identification String messageId = UUID.randomUUID().toString(); + response.setMessageId(messageId); - // fake SMS message delivery for null user ID + // Fake SMS message delivery for null user ID in case of non-existent account or blocked user account if (userId == null) { - return messageId; + // Make sure that user cannot recognize that the SMS was not sent, even the result is sent as fake success + response.setSmsDeliveryResult(SmsDeliveryResult.SUCCEEDED); + return response; } - // generate authorization code - AuthorizationCode authorizationCode = generateAuthorizationCode(userId, organizationId, operationContext); + // Generate authorization code + AuthorizationCode authorizationCode = smsDeliveryService.generateAuthorizationCode(userId, organizationId, operationContext); - // generate message text, include previously generated authorization code - String messageText = generateSmsText(userId, organizationId, operationContext, authorizationCode, lang); + // Generate message text, include previously generated authorization code + String messageText = smsDeliveryService.generateSmsText(userId, organizationId, operationContext, authorizationCode, lang); - // persist authorization SMS message + // Persist authorization SMS message smsPersistenceService.createAuthorizationSms(userId, organizationId, messageId, operationContext, authorizationCode, messageText); // Send SMS with generated text to target user. - sendAuthorizationSms(userId, organizationId, messageId, messageText, operationContext); - - // return generated message ID - return messageId; - } - - @Override - public AuthorizationCode generateAuthorizationCode(String userId, String organizationId, OperationContext operationContext) throws InvalidOperationContextException { - String operationName = operationContext.getName(); - List digestItems = new ArrayList<>(); - switch (operationName) { - case "login": - case "login_sca": { - digestItems.add(operationName); - break; - } - case "authorize_payment": - case "authorize_payment_sca": { - AmountAttribute amountAttribute = operationValueExtractionService.getAmount(operationContext); - String account = operationValueExtractionService.getAccount(operationContext); - BigDecimal amount = amountAttribute.getAmount(); - String currency = amountAttribute.getCurrency(); - digestItems.add(amount.toPlainString()); - digestItems.add(currency); - digestItems.add(account); - break; - } - // Add new operations here. - default: - throw new InvalidOperationContextException("Unsupported operation: " + operationName); + SmsDeliveryResult deliveryResult = smsDeliveryService.sendAuthorizationSms(userId, organizationId, messageId, messageText, operationContext); + response.setSmsDeliveryResult(deliveryResult); + if (!SmsDeliveryResult.SUCCEEDED.equals(deliveryResult)) { + response.setErrorMessage(SMS_DELIVERY_FAILED); } - final DataDigest.Result digestResult = new DataDigest().generateDigest(digestItems); - if (digestResult == null) { - throw new InvalidOperationContextException("Digest generation failed"); - } - return new AuthorizationCode(digestResult.getDigest(), digestResult.getSalt()); - } - - @Override - public String generateSmsText(String userId, String organizationId, OperationContext operationContext, AuthorizationCode authorizationCode, String lang) throws InvalidOperationContextException { - String operationName = operationContext.getName(); - String[] messageArgs; - switch (operationName) { - case "login": - case "login_sca": { - messageArgs = new String[]{authorizationCode.getCode()}; - break; - } - case "authorize_payment": - case "authorize_payment_sca": { - AmountAttribute amountAttribute = operationValueExtractionService.getAmount(operationContext); - String account = operationValueExtractionService.getAccount(operationContext); - BigDecimal amount = amountAttribute.getAmount(); - String currency = amountAttribute.getCurrency(); - messageArgs = new String[]{amount.toPlainString(), currency, account, authorizationCode.getCode()}; - break; - } - // Add new operations here. - default: - throw new InvalidOperationContextException("Unsupported operation: " + operationName); - } - - return dataAdapterI18NService.messageSource().getMessage(operationName + ".smsText", messageArgs, new Locale(lang)); - } - - @Override - public void sendAuthorizationSms(String userId, String organizationId, String messageId, String messageText, OperationContext operationContext) throws DataAdapterRemoteException { - // Add here code to send the SMS OTP message to user identified by userId with messageText. - // The message entity can be extracted using message ID from table da_sms_authorization. - // In case message delivery fails, throw a DataAdapterRemoteException. + // Return generated message ID + return response; } @Override @@ -314,7 +256,7 @@ public VerifySmsAndPasswordResponse verifyAuthorizationSmsAndPassword(String use if (smsResponse.getSmsAuthorizationResult() != SmsAuthorizationResult.VERIFIED_SUCCEEDED || authResponse.getAuthenticationResult() != UserAuthenticationResult.VERIFIED_SUCCEEDED) { // Provide an error message which does not allow to find out reason of failed verification. - response.setErrorMessage("login.authenticationFailed"); + response.setErrorMessage(AUTHENTICATION_FAILED); } // Optionally set the number of remaining attempts, e.g. using lower of the two remaining attempt counts. // response.setRemainingAttempts(Math.min(smsResponse.getRemainingAttempts(), authResponse.getRemainingAttempts())); diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/SmsDeliveryService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/SmsDeliveryService.java new file mode 100644 index 00000000..5976d97a --- /dev/null +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/SmsDeliveryService.java @@ -0,0 +1,145 @@ +/* + * Copyright 2019 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getlime.security.powerauth.app.dataadapter.impl.service; + +import io.getlime.security.powerauth.app.dataadapter.exception.DataAdapterRemoteException; +import io.getlime.security.powerauth.app.dataadapter.exception.InvalidOperationContextException; +import io.getlime.security.powerauth.app.dataadapter.service.DataAdapterI18NService; +import io.getlime.security.powerauth.crypto.server.util.DataDigest; +import io.getlime.security.powerauth.lib.dataadapter.model.entity.AuthorizationCode; +import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; +import io.getlime.security.powerauth.lib.dataadapter.model.entity.attribute.AmountAttribute; +import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.SmsDeliveryResult; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * Service for preparing and delivering SMS messages. + * + * @author Roman Strobl, roman.strobl@wultra.com + */ +@Service +public class SmsDeliveryService { + + private final DataAdapterI18NService dataAdapterI18NService; + private final OperationValueExtractionService operationValueExtractionService; + + /** + * Service constructor. + * @param dataAdapterI18NService I18N service. + * @param operationValueExtractionService Service for extracting values from operation. + */ + public SmsDeliveryService(DataAdapterI18NService dataAdapterI18NService, OperationValueExtractionService operationValueExtractionService) { + this.dataAdapterI18NService = dataAdapterI18NService; + this.operationValueExtractionService = operationValueExtractionService; + } + + /** + * Generate authorization code for SMS authorization. + * @param userId User ID. + * @param organizationId Organization ID. + * @param operationContext Operation context. + * @return Authorization code. + * @throws InvalidOperationContextException Thrown when operation context is invalid. + * @throws DataAdapterRemoteException Thrown when remote communication fails. + */ + public AuthorizationCode generateAuthorizationCode(String userId, String organizationId, OperationContext operationContext) throws InvalidOperationContextException, DataAdapterRemoteException { + String operationName = operationContext.getName(); + List digestItems = new ArrayList<>(); + switch (operationName) { + case "login": { + digestItems.add(operationName); + break; + } + case "authorize_payment": { + AmountAttribute amountAttribute = operationValueExtractionService.getAmount(operationContext); + String account = operationValueExtractionService.getAccount(operationContext); + BigDecimal amount = amountAttribute.getAmount(); + String currency = amountAttribute.getCurrency(); + digestItems.add(amount.toPlainString()); + digestItems.add(currency); + digestItems.add(account); + break; + } + // Add new operations here. + default: + throw new InvalidOperationContextException("Unsupported operation: " + operationName); + } + + final DataDigest.Result digestResult = new DataDigest().generateDigest(digestItems); + if (digestResult == null) { + throw new InvalidOperationContextException("Digest generation failed"); + } + return new AuthorizationCode(digestResult.getDigest(), digestResult.getSalt()); + } + + /** + * Generate text for SMS authorization message. + * @param userId User ID. + * @param organizationId Organization ID. + * @param operationContext Operation context. + * @param authorizationCode Authorization code. + * @param lang Language for localization. + * @return Generated SMS text with authorization code. + * @throws InvalidOperationContextException Thrown when operation context is invalid. + * @throws DataAdapterRemoteException Thrown when remote communication fails. + */ + public String generateSmsText(String userId, String organizationId, OperationContext operationContext, AuthorizationCode authorizationCode, String lang) throws InvalidOperationContextException, DataAdapterRemoteException { + String operationName = operationContext.getName(); + String[] messageArgs; + switch (operationName) { + case "login": { + messageArgs = new String[]{authorizationCode.getCode()}; + break; + } + case "authorize_payment": { + AmountAttribute amountAttribute = operationValueExtractionService.getAmount(operationContext); + String account = operationValueExtractionService.getAccount(operationContext); + BigDecimal amount = amountAttribute.getAmount(); + String currency = amountAttribute.getCurrency(); + messageArgs = new String[]{amount.toPlainString(), currency, account, authorizationCode.getCode()}; + break; + } + // Add new operations here. + default: + throw new InvalidOperationContextException("Unsupported operation: " + operationName); + } + + return dataAdapterI18NService.messageSource().getMessage(operationName + ".smsText", messageArgs, new Locale(lang)); + } + + /** + * Send an authorization SMS with generated authorization code. + * @param userId User ID. + * @param organizationId Organization ID. + * @param messageId Message ID. + * @param messageText Text of SMS message. + * @param operationContext Operation context. + * @throws InvalidOperationContextException Thrown when operation context is invalid. + * @throws DataAdapterRemoteException Thrown when remote communication fails or SMS message could not be delivered. + */ + public SmsDeliveryResult sendAuthorizationSms(String userId, String organizationId, String messageId, String messageText, OperationContext operationContext) throws InvalidOperationContextException, DataAdapterRemoteException { + // Add here code to send the SMS OTP message to user identified by userId with messageText. + // The message entity can be extracted using message ID from table da_sms_authorization. + // In case message delivery fails, throw a DataAdapterRemoteException. + return SmsDeliveryResult.SUCCEEDED; + } + +} From 84561eb3a095b7d79b2dc45a2806c19eb140b9c1 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Thu, 25 Jul 2019 20:23:10 +0200 Subject: [PATCH 51/79] Add default SCA operation names --- .../dataadapter/impl/service/SmsDeliveryService.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/SmsDeliveryService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/SmsDeliveryService.java index 5976d97a..3bea4393 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/SmsDeliveryService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/SmsDeliveryService.java @@ -64,11 +64,13 @@ public AuthorizationCode generateAuthorizationCode(String userId, String organiz String operationName = operationContext.getName(); List digestItems = new ArrayList<>(); switch (operationName) { - case "login": { + case "login": + case "login_sca": { digestItems.add(operationName); break; } - case "authorize_payment": { + case "authorize_payment": + case "authorize_payment_sca": { AmountAttribute amountAttribute = operationValueExtractionService.getAmount(operationContext); String account = operationValueExtractionService.getAccount(operationContext); BigDecimal amount = amountAttribute.getAmount(); @@ -105,11 +107,13 @@ public String generateSmsText(String userId, String organizationId, OperationCon String operationName = operationContext.getName(); String[] messageArgs; switch (operationName) { - case "login": { + case "login": + case "login_sca": { messageArgs = new String[]{authorizationCode.getCode()}; break; } - case "authorize_payment": { + case "authorize_payment": + case "authorize_payment_sca": { AmountAttribute amountAttribute = operationValueExtractionService.getAmount(operationContext); String account = operationValueExtractionService.getAccount(operationContext); BigDecimal amount = amountAttribute.getAmount(); From fb2bd12793227e424360b42927660752a396b0aa Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Fri, 13 Sep 2019 18:08:43 +0200 Subject: [PATCH 52/79] Fix #100: Update version --- powerauth-data-adapter/pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/powerauth-data-adapter/pom.xml b/powerauth-data-adapter/pom.xml index 4c27a27b..3c790039 100644 --- a/powerauth-data-adapter/pom.xml +++ b/powerauth-data-adapter/pom.xml @@ -5,7 +5,7 @@ powerauth-data-adapter io.getlime.security - 0.22.0 + 0.23.0-SNAPSHOT war powerauth-data-adapter @@ -89,12 +89,12 @@ io.getlime.security powerauth-data-adapter-model - 0.22.0 + 0.23.0-SNAPSHOT io.getlime.security powerauth-java-crypto - 0.22.0 + 0.23.0-SNAPSHOT From bd1e4e0d0c1cc99e50bbb937d32da8b4323a925f Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Fri, 13 Sep 2019 19:08:43 +0200 Subject: [PATCH 53/79] Fix Guava vulnerability in dependencies for Swagger --- powerauth-data-adapter/pom.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/powerauth-data-adapter/pom.xml b/powerauth-data-adapter/pom.xml index 3c790039..b4fd6826 100644 --- a/powerauth-data-adapter/pom.xml +++ b/powerauth-data-adapter/pom.xml @@ -133,6 +133,12 @@ springfox-swagger-ui 2.9.2 + + + com.google.guava + guava + 27.0.1-jre + From 1737d9403035f69728c52c5261c12f1954630e55 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Mon, 16 Sep 2019 16:08:46 +0200 Subject: [PATCH 54/79] Fix #104: Handle unsupported language in consent form --- .../dataadapter/impl/service/DataAdapterService.java | 8 ++++++++ .../impl/validation/ConsentFormRequestValidator.java | 10 ++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index 38c90842..03dfe69a 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -273,6 +273,10 @@ public InitConsentFormResponse initConsentForm(String userId, String organizatio @Override public CreateConsentFormResponse createConsentForm(String userId, String organizationId, OperationContext operationContext, String lang) throws DataAdapterRemoteException, InvalidOperationContextException { + // Fallback to English for unsupported languages, see: https://github.com/wultra/powerauth-webflow-customization/issues/104 + if (!"cs".equals(lang) && !"en".equals(lang)) { + lang = "en"; + } // Generate response with consent text and options based on requested language. if ("login".equals(operationContext.getName()) || "login_sca".equals(operationContext.getName())) { // Create default consent @@ -330,6 +334,10 @@ public CreateConsentFormResponse createConsentForm(String userId, String organiz @Override public ValidateConsentFormResponse validateConsentForm(String userId, String organizationId, OperationContext operationContext, String lang, List options) throws DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException { + // Fallback to English for unsupported languages, see: https://github.com/wultra/powerauth-webflow-customization/issues/104 + if (!"cs".equals(lang) && !"en".equals(lang)) { + lang = "en"; + } // Validate consent form options and return response with result of validation and optional error messages. ValidateConsentFormResponse response = new ValidateConsentFormResponse(); if (options == null || options.isEmpty()) { diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/ConsentFormRequestValidator.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/ConsentFormRequestValidator.java index 7eb606ab..d7ad2828 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/ConsentFormRequestValidator.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/ConsentFormRequestValidator.java @@ -82,7 +82,7 @@ public void validate(@Nullable Object o, @NonNull Errors errors) { if (request.getOperationContext() != null) { validateOperationName(request.getOperationContext().getName(), errors); } - validateLanguage(request.getLang(), errors); + validateLanguage(errors); } else if (objectRequest.getRequestObject() instanceof ValidateConsentFormRequest) { ObjectRequest requestObject = (ObjectRequest) o; ValidateConsentFormRequest request = requestObject.getRequestObject(); @@ -91,7 +91,7 @@ public void validate(@Nullable Object o, @NonNull Errors errors) { if (request.getOperationContext() != null) { validateOperationName(request.getOperationContext().getName(), errors); } - validateLanguage(request.getLang(), errors); + validateLanguage(errors); validateOptions(request.getOptions(), errors); } else if (objectRequest.getRequestObject() instanceof SaveConsentFormRequest) { ObjectRequest requestObject = (ObjectRequest) o; @@ -125,11 +125,9 @@ private void validateOperationName(String operationName, Errors errors) { } } - private void validateLanguage(String lang, Errors errors) { + private void validateLanguage(Errors errors) { ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.lang", "consent.invalidRequest"); - if (lang != null && !lang.equals("cs") && !lang.equals("en")) { - errors.rejectValue("requestObject.lang", "consent.invalidRequest"); - } + // Do not validate lang parameter, fallback to "en" instead, see: https://github.com/wultra/powerauth-webflow-customization/issues/104 } private void validateOptions(List options, Errors errors) { From 91efd0fb5bd69e3eb92aa2d779456de87a92da8c Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Tue, 24 Sep 2019 17:49:42 +0200 Subject: [PATCH 55/79] Fix #99: AFS: Update Data Adapter API and provide sample implementation --- .../app/dataadapter/api/DataAdapter.java | 18 ++++ .../dataadapter/controller/AfsController.java | 82 +++++++++++++++++++ .../impl/service/DataAdapterService.java | 39 ++++++++- .../src/main/resources/application.properties | 9 ++ 4 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AfsController.java diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java index d59d4bcc..21d2eb19 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java @@ -17,9 +17,12 @@ import io.getlime.security.powerauth.app.dataadapter.exception.*; import io.getlime.security.powerauth.lib.dataadapter.model.entity.*; +import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.AuthInstrument; +import io.getlime.security.powerauth.lib.dataadapter.model.request.AfsRequestParameters; import io.getlime.security.powerauth.lib.dataadapter.model.response.*; import java.util.List; +import java.util.Map; /** * Interface defines methods which should be implemented for integration of Web Flow with 3rd parties. @@ -176,10 +179,25 @@ public interface DataAdapter { * @param organizationId Organization ID. * @param operationContext Operation context. * @param options Options selected by the user. + * @return Response with result of saving the consent form. * @throws DataAdapterRemoteException Thrown when remote communication fails. * @throws InvalidOperationContextException Thrown when operation context is invalid. * @throws InvalidConsentDataException In case consent options are invalid. */ SaveConsentFormResponse saveConsentForm(String userId, String organizationId, OperationContext operationContext, List options) throws DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException; + /** + * Execute an anti-fraud system action and return response for usage in Web Flow. + * @param userId User ID. + * @param organizationId Organization ID. + * @param operationContext Operation context. + * @param afsRequestParameters Request parameters for AFS. + * @param authInstruments Authentication instruments used during this authentication step. + * @param extras Extra parameters for AFS. + * @return Response from AFS for usage in Web Flow. + * @throws DataAdapterRemoteException Thrown when remote communication fails. + * @throws InvalidOperationContextException Thrown when operation context is invalid. + */ + AfsResponse executeAfsAction(String userId, String organizationId, OperationContext operationContext, AfsRequestParameters afsRequestParameters, List authInstruments, Map extras) throws DataAdapterRemoteException, InvalidOperationContextException; + } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AfsController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AfsController.java new file mode 100644 index 00000000..4aa0674b --- /dev/null +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AfsController.java @@ -0,0 +1,82 @@ +/* + * Copyright 2019 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getlime.security.powerauth.app.dataadapter.controller; + +import io.getlime.core.rest.model.base.request.ObjectRequest; +import io.getlime.core.rest.model.base.response.ObjectResponse; +import io.getlime.security.powerauth.app.dataadapter.api.DataAdapter; +import io.getlime.security.powerauth.app.dataadapter.exception.DataAdapterRemoteException; +import io.getlime.security.powerauth.app.dataadapter.exception.InvalidOperationContextException; +import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; +import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.AuthInstrument; +import io.getlime.security.powerauth.lib.dataadapter.model.request.*; +import io.getlime.security.powerauth.lib.dataadapter.model.response.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * Controller class which handles OAuth 2.0 consent actions. + * + * @author Roman Strobl, roman.strobl@wultra.com + */ +@RestController +@RequestMapping("/api/afs") +public class AfsController { + + private static final Logger logger = LoggerFactory.getLogger(AfsController.class); + + private final DataAdapter dataAdapter; + + /** + * Consent controller constructor. + * @param dataAdapter Data adapter. + */ + @Autowired + public AfsController(DataAdapter dataAdapter) { + this.dataAdapter = dataAdapter; + } + + // TODO - implement validator + + /** + * Execute an anti-fraud system action and return response for usage in Web Flow. + * @param request AFS request. + * @return AFS response. + * @throws DataAdapterRemoteException In case communication with remote system fails. + * @throws InvalidOperationContextException In case operation context is invalid. + */ + @RequestMapping(value = "/action", method = RequestMethod.POST) + public ObjectResponse executeAfsAction(@RequestBody ObjectRequest request) throws DataAdapterRemoteException, InvalidOperationContextException { + logger.info("Received executeAfsAction request for user: {}, operation ID: {}", + request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); + AfsRequest afsRequest = request.getRequestObject(); + String userId = afsRequest.getUserId(); + String organizationId = afsRequest.getOrganizationId(); + OperationContext operationContext = afsRequest.getOperationContext(); + AfsRequestParameters requestParameters = afsRequest.getAfsRequestParameters(); + List authInstruments = afsRequest.getAuthInstruments(); + Map extras = afsRequest.getExtras(); + AfsResponse response = dataAdapter.executeAfsAction(userId, organizationId, operationContext, requestParameters, authInstruments, extras); + logger.debug("The executeAfsAction request succeeded"); + return new ObjectResponse<>(response); + } + +} diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index 03dfe69a..f9cfb50c 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -6,10 +6,8 @@ import io.getlime.security.powerauth.app.dataadapter.service.SmsPersistenceService; import io.getlime.security.powerauth.lib.dataadapter.model.entity.*; import io.getlime.security.powerauth.lib.dataadapter.model.entity.attribute.FormFieldConfig; -import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.PasswordProtectionType; -import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.SmsAuthorizationResult; -import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.SmsDeliveryResult; -import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.UserAuthenticationResult; +import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.*; +import io.getlime.security.powerauth.lib.dataadapter.model.request.AfsRequestParameters; import io.getlime.security.powerauth.lib.dataadapter.model.response.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -20,6 +18,7 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.UUID; /** @@ -431,4 +430,36 @@ public SaveConsentFormResponse saveConsentForm(String userId, String organizatio return new SaveConsentFormResponse(true); } + @Override + public AfsResponse executeAfsAction(String userId, String organizationId, OperationContext operationContext, AfsRequestParameters afsRequestParameters, List authInstruments, Map extras) throws DataAdapterRemoteException, InvalidOperationContextException { + // Call anti-fraud system and return response for Web Flow. + // In default implementation of Data Adapter a mocked response is returned with static 2FA AFS label. + AfsResponse response = new AfsResponse(); + switch (afsRequestParameters.getAfsAction()) { + case LOGIN_INIT: + case APPROVAL_AUTH: + // Return AFS label, but do not apply response parameters on authentication form + response.setApplyAfsResponse(false); + response.setAfsLabel("2FA"); + break; + + case APPROVAL_INIT: + // Apply AFS response parameters on authentication form. + // This example performs step-down from 2FA to 1FA. + response.setApplyAfsResponse(true); + response.setAfsLabel("1FA"); + response.getAuthStepOptions().setPasswordRequired(false); + response.getAuthStepOptions().setSmsOtpRequired(true); + break; + + case LOGIN_AUTH: + case LOGOUT: + // Do not apply response parameters + response.setApplyAfsResponse(false); + break; + + } + return response; + } + } diff --git a/powerauth-data-adapter/src/main/resources/application.properties b/powerauth-data-adapter/src/main/resources/application.properties index cde4c112..9dac2721 100644 --- a/powerauth-data-adapter/src/main/resources/application.properties +++ b/powerauth-data-adapter/src/main/resources/application.properties @@ -14,6 +14,15 @@ spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.jpa.properties.hibernate.connection.characterEncoding=utf8 spring.jpa.properties.hibernate.connection.useUnicode=true +# Database Configuration - PostgreSQL +#spring.datasource.url=jdbc:postgresql://localhost:5432/postgres +#spring.datasource.username=powerauth +#spring.datasource.password= +#spring.datasource.driver-class-name=org.postgresql.Driver +#spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false +#spring.jpa.properties.hibernate.connection.characterEncoding=utf8 +#spring.jpa.properties.hibernate.connection.useUnicode=true + # Database Configuration - Oracle #spring.datasource.url=jdbc:oracle:thin:@//localhost:1521/powerauth #spring.datasource.username=powerauth From c5cab8408e9cd3749a995c8bc64330764abced02 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Wed, 25 Sep 2019 12:48:26 +0200 Subject: [PATCH 56/79] Improve AFS sample --- .../impl/service/DataAdapterService.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index f9cfb50c..fb29426f 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -5,6 +5,7 @@ import io.getlime.security.powerauth.app.dataadapter.service.DataAdapterI18NService; import io.getlime.security.powerauth.app.dataadapter.service.SmsPersistenceService; import io.getlime.security.powerauth.lib.dataadapter.model.entity.*; +import io.getlime.security.powerauth.lib.dataadapter.model.entity.attribute.AmountAttribute; import io.getlime.security.powerauth.lib.dataadapter.model.entity.attribute.FormFieldConfig; import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.*; import io.getlime.security.powerauth.lib.dataadapter.model.request.AfsRequestParameters; @@ -445,11 +446,19 @@ public AfsResponse executeAfsAction(String userId, String organizationId, Operat case APPROVAL_INIT: // Apply AFS response parameters on authentication form. - // This example performs step-down from 2FA to 1FA. - response.setApplyAfsResponse(true); - response.setAfsLabel("1FA"); - response.getAuthStepOptions().setPasswordRequired(false); - response.getAuthStepOptions().setSmsOtpRequired(true); + // This example performs step-down from 2FA to 1FA in case of payment in CZK with low amount. + AmountAttribute amountAttr = operationContext.getFormData().getAmount(); + if (amountAttr.getCurrency().equals("CZK") && amountAttr.getAmount().intValue() < 500) { + // Disable password verification for low amounts + response.setApplyAfsResponse(true); + response.setAfsLabel("1FA"); + response.getAuthStepOptions().setPasswordRequired(false); + response.getAuthStepOptions().setSmsOtpRequired(true); + } else { + // For higher amounts keep the password verification + response.setApplyAfsResponse(false); + response.setAfsLabel("2FA"); + } break; case LOGIN_AUTH: From 460874bcea42751483063598a31f82deb310ffad Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Wed, 25 Sep 2019 16:00:34 +0200 Subject: [PATCH 57/79] Use developer friendly method names --- .../app/dataadapter/impl/service/DataAdapterService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index fb29426f..09d83724 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -440,7 +440,7 @@ public AfsResponse executeAfsAction(String userId, String organizationId, Operat case LOGIN_INIT: case APPROVAL_AUTH: // Return AFS label, but do not apply response parameters on authentication form - response.setApplyAfsResponse(false); + response.setAfsResponseApplied(false); response.setAfsLabel("2FA"); break; @@ -450,13 +450,13 @@ public AfsResponse executeAfsAction(String userId, String organizationId, Operat AmountAttribute amountAttr = operationContext.getFormData().getAmount(); if (amountAttr.getCurrency().equals("CZK") && amountAttr.getAmount().intValue() < 500) { // Disable password verification for low amounts - response.setApplyAfsResponse(true); + response.setAfsResponseApplied(true); response.setAfsLabel("1FA"); response.getAuthStepOptions().setPasswordRequired(false); response.getAuthStepOptions().setSmsOtpRequired(true); } else { // For higher amounts keep the password verification - response.setApplyAfsResponse(false); + response.setAfsResponseApplied(false); response.setAfsLabel("2FA"); } break; @@ -464,7 +464,7 @@ public AfsResponse executeAfsAction(String userId, String organizationId, Operat case LOGIN_AUTH: case LOGOUT: // Do not apply response parameters - response.setApplyAfsResponse(false); + response.setAfsResponseApplied(false); break; } From 70baa025d353d9098ae598ebc2b13ec71bf9f28e Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Wed, 25 Sep 2019 20:23:01 +0200 Subject: [PATCH 58/79] Update comments --- .../app/dataadapter/impl/service/DataAdapterService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index 09d83724..f5f8df11 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -433,8 +433,8 @@ public SaveConsentFormResponse saveConsentForm(String userId, String organizatio @Override public AfsResponse executeAfsAction(String userId, String organizationId, OperationContext operationContext, AfsRequestParameters afsRequestParameters, List authInstruments, Map extras) throws DataAdapterRemoteException, InvalidOperationContextException { - // Call anti-fraud system and return response for Web Flow. - // In default implementation of Data Adapter a mocked response is returned with static 2FA AFS label. + // Call anti-fraud system and return response for Web Flow. In default implementation of Data Adapter + // a mocked response is returned with static 2FA AFS label except for the case of payment with low amount. AfsResponse response = new AfsResponse(); switch (afsRequestParameters.getAfsAction()) { case LOGIN_INIT: From 98cd84289c91696d9f00e27171b00f05bf82950d Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Thu, 26 Sep 2019 17:40:02 +0200 Subject: [PATCH 59/79] Use less arguments in Data Adapter call for AFS --- .../security/powerauth/app/dataadapter/api/DataAdapter.java | 3 +-- .../powerauth/app/dataadapter/controller/AfsController.java | 3 +-- .../app/dataadapter/impl/service/DataAdapterService.java | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java index 21d2eb19..8bbaa74a 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java @@ -192,12 +192,11 @@ public interface DataAdapter { * @param organizationId Organization ID. * @param operationContext Operation context. * @param afsRequestParameters Request parameters for AFS. - * @param authInstruments Authentication instruments used during this authentication step. * @param extras Extra parameters for AFS. * @return Response from AFS for usage in Web Flow. * @throws DataAdapterRemoteException Thrown when remote communication fails. * @throws InvalidOperationContextException Thrown when operation context is invalid. */ - AfsResponse executeAfsAction(String userId, String organizationId, OperationContext operationContext, AfsRequestParameters afsRequestParameters, List authInstruments, Map extras) throws DataAdapterRemoteException, InvalidOperationContextException; + AfsResponse executeAfsAction(String userId, String organizationId, OperationContext operationContext, AfsRequestParameters afsRequestParameters, Map extras) throws DataAdapterRemoteException, InvalidOperationContextException; } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AfsController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AfsController.java index 4aa0674b..6c6e26a8 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AfsController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AfsController.java @@ -72,9 +72,8 @@ public ObjectResponse executeAfsAction(@RequestBody ObjectRequest authInstruments = afsRequest.getAuthInstruments(); Map extras = afsRequest.getExtras(); - AfsResponse response = dataAdapter.executeAfsAction(userId, organizationId, operationContext, requestParameters, authInstruments, extras); + AfsResponse response = dataAdapter.executeAfsAction(userId, organizationId, operationContext, requestParameters, extras); logger.debug("The executeAfsAction request succeeded"); return new ObjectResponse<>(response); } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index f5f8df11..fcd4aba4 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -432,7 +432,7 @@ public SaveConsentFormResponse saveConsentForm(String userId, String organizatio } @Override - public AfsResponse executeAfsAction(String userId, String organizationId, OperationContext operationContext, AfsRequestParameters afsRequestParameters, List authInstruments, Map extras) throws DataAdapterRemoteException, InvalidOperationContextException { + public AfsResponse executeAfsAction(String userId, String organizationId, OperationContext operationContext, AfsRequestParameters afsRequestParameters, Map extras) throws DataAdapterRemoteException, InvalidOperationContextException { // Call anti-fraud system and return response for Web Flow. In default implementation of Data Adapter // a mocked response is returned with static 2FA AFS label except for the case of payment with low amount. AfsResponse response = new AfsResponse(); From c2906e67dfc180f73f0b0bf8d26dc1d95eb61285 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Thu, 10 Oct 2019 13:28:34 +0200 Subject: [PATCH 60/79] Code cleanup --- .../controller/AuthenticationController.java | 6 +- .../controller/ConsentController.java | 8 +- .../controller/FormDataChangeController.java | 4 +- .../controller/OperationChangeController.java | 2 +- .../controller/ServiceController.java | 4 +- .../SmsAuthorizationController.java | 6 +- .../exception/DefaultExceptionResolver.java | 95 +++++++++------ .../AuthenticationRequestValidator.java | 114 ++++++++++-------- .../ConsentFormRequestValidator.java | 28 +++-- ...reateSmsAuthorizationRequestValidator.java | 93 +++++++------- .../service/SmsPersistenceService.java | 1 - 11 files changed, 203 insertions(+), 158 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java index 90b19800..9b7b2c18 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java @@ -78,7 +78,7 @@ private void initBinder(WebDataBinder binder) { * @throws DataAdapterRemoteException Thrown in case of remote communication errors. * @throws UserNotFoundException Thrown in case that user does not exist. */ - @RequestMapping(value = "/lookup", method = RequestMethod.POST) + @PostMapping(value = "/lookup") public ObjectResponse lookupUser(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException, UserNotFoundException { logger.info("Received user lookup request, username: {}, organization ID: {}, operation ID: {}", request.getRequestObject().getUsername(), request.getRequestObject().getOrganizationId(), @@ -100,7 +100,7 @@ public ObjectResponse lookupUser(@Valid @RequestBody ObjectR * @return Response with authenticated user ID. * @throws DataAdapterRemoteException Thrown in case of remote communication errors. */ - @RequestMapping(value = "/authenticate", method = RequestMethod.POST) + @PostMapping(value = "/authenticate") public ObjectResponse authenticate(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException { logger.info("Received authenticate request, user ID: {}, organization ID: {}, operation ID: {}", request.getRequestObject().getUserId(), request.getRequestObject().getOrganizationId(), @@ -125,7 +125,7 @@ public ObjectResponse authenticate(@Valid @RequestBo * @throws DataAdapterRemoteException Thrown in case of remote communication errors. * @throws UserNotFoundException Thrown in case user is not found. */ - @RequestMapping(value = "/info", method = RequestMethod.POST) + @PostMapping(value = "/info") public ObjectResponse fetchUserDetail(@RequestBody ObjectRequest request) throws DataAdapterRemoteException, UserNotFoundException { logger.info("Received fetchUserDetail request, user ID: {}", request.getRequestObject().getUserId()); UserDetailRequest userDetailRequest = request.getRequestObject(); diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ConsentController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ConsentController.java index 1752138a..a9bd2d63 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ConsentController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ConsentController.java @@ -83,7 +83,7 @@ private void initBinder(WebDataBinder binder) { * @throws DataAdapterRemoteException In case communication with remote system fails. * @throws InvalidOperationContextException In case operation context is invalid. */ - @RequestMapping(value = "/init", method = RequestMethod.POST) + @PostMapping(value = "/init") public ObjectResponse initConsentForm(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException, InvalidOperationContextException { logger.info("Received initConsentForm request for user: {}, operation ID: {}", request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); @@ -103,7 +103,7 @@ public ObjectResponse initConsentForm(@Valid @RequestBo * @throws DataAdapterRemoteException In case communication with remote system fails. * @throws InvalidOperationContextException In case operation context is invalid. */ - @RequestMapping(value = "/create", method = RequestMethod.POST) + @PostMapping(value = "/create") public ObjectResponse createConsentForm(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException, InvalidOperationContextException { logger.info("Received createConsentForm request for user: {}, operation ID: {}", request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); @@ -125,7 +125,7 @@ public ObjectResponse createConsentForm(@Valid @Reque * @throws InvalidOperationContextException In case operation context is invalid. * @throws InvalidConsentDataException In case consent options are invalid. */ - @RequestMapping(value = "/validate", method = RequestMethod.POST) + @PostMapping(value = "/validate") public ObjectResponse validateConsentForm(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException { logger.info("Received validateConsentForm request for user: {}, operation ID: {}", request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); @@ -148,7 +148,7 @@ public ObjectResponse validateConsentForm(@Valid @R * @throws InvalidOperationContextException In case operation context is invalid. * @throws InvalidConsentDataException In case consent options are invalid. */ - @RequestMapping(value = "/save", method = RequestMethod.POST) + @PostMapping(value = "/save") public ObjectResponse saveConsentForm(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException { logger.info("Received saveConsentForm request for user: {}, operation ID: {}", request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/FormDataChangeController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/FormDataChangeController.java index c6dd7e2e..f387bc1d 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/FormDataChangeController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/FormDataChangeController.java @@ -60,7 +60,7 @@ public FormDataChangeController(DataAdapter dataAdapter) { * @return Object response. * @throws DataAdapterRemoteException Thrown in case of remote communication errors. */ - @RequestMapping(value = "/change", method = RequestMethod.POST) + @PostMapping(value = "/change") public Response formDataChangedNotification(@RequestBody ObjectRequest request) throws DataAdapterRemoteException { logger.info("Received formDataChangedNotification request for user: {}, operation ID: {}", request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); @@ -82,7 +82,7 @@ public Response formDataChangedNotification(@RequestBody ObjectRequest decorateOperationFormData(@RequestBody ObjectRequest request) throws DataAdapterRemoteException, UserNotFoundException { logger.info("Received decorateOperationFormData request for user: {}, operation ID: {}", request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/OperationChangeController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/OperationChangeController.java index 01112245..7f48b9f4 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/OperationChangeController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/OperationChangeController.java @@ -56,7 +56,7 @@ public OperationChangeController(DataAdapter dataAdapter) { * @return Object response. * @throws DataAdapterRemoteException Thrown in case of remote communication errors. */ - @RequestMapping(value = "/change", method = RequestMethod.POST) + @PostMapping(value = "/change") public Response operationChangedNotification(@RequestBody ObjectRequest request) throws DataAdapterRemoteException { logger.info("Received operationChangedNotification request for user: {}, operation ID: {}", request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ServiceController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ServiceController.java index 15403db8..13377134 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ServiceController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ServiceController.java @@ -24,8 +24,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.info.BuildProperties; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.GetMapping; import java.util.Date; @@ -58,7 +58,7 @@ public ServiceController(DataAdapterConfiguration dataAdapterConfiguration, Buil * Controller resource with system information. * @return System status info. */ - @RequestMapping(value = "status", method = RequestMethod.GET) + @GetMapping(value = "status") public ObjectResponse getServiceStatus() { logger.info("Received getServiceStatus request"); ServiceStatusResponse response = new ServiceStatusResponse(); diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java index 64897aa3..fb4d881c 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java @@ -79,7 +79,7 @@ private void initBinder(WebDataBinder binder) { * @throws DataAdapterRemoteException Thrown in case of remote communication errors. * @throws InvalidOperationContextException Thrown in case operation context is invalid. */ - @RequestMapping(value = "create", method = RequestMethod.POST) + @PostMapping(value = "create") public ObjectResponse createAuthorizationSms(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException, InvalidOperationContextException { logger.info("Received createAuthorizationSms request, operation ID: {}", request.getRequestObject().getOperationContext().getId()); CreateSmsAuthorizationRequest smsRequest = request.getRequestObject(); @@ -103,7 +103,7 @@ public ObjectResponse createAuthorizationSms(@Va * @throws DataAdapterRemoteException Thrown in case communication with remote system fails. * @throws InvalidOperationContextException Thrown in case operation context is invalid. */ - @RequestMapping(value = "verify", method = RequestMethod.POST) + @PostMapping(value = "verify") public ObjectResponse verifyAuthorizationSms(@RequestBody ObjectRequest request) throws InvalidOperationContextException, DataAdapterRemoteException { logger.info("Received verifyAuthorizationSms request, operation ID: {}", request.getRequestObject().getOperationContext().getId()); VerifySmsAuthorizationRequest verifyRequest = request.getRequestObject(); @@ -126,7 +126,7 @@ public ObjectResponse verifyAuthorizationSms(@Re * @throws DataAdapterRemoteException Thrown in case communication with remote system fails. * @throws InvalidOperationContextException Thrown in case operation context is invalid. */ - @RequestMapping(value = "/password/verify", method = RequestMethod.POST) + @PostMapping(value = "/password/verify") public ObjectResponse verifyAuthorizationSmsAndPassword(@RequestBody ObjectRequest request) throws DataAdapterRemoteException, InvalidOperationContextException { logger.info("Received verifyAuthorizationSmsAndPassword request, operation ID: {}", request.getRequestObject().getOperationContext().getId()); VerifySmsAndPasswordRequest verifyRequest = request.getRequestObject(); diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/DefaultExceptionResolver.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/DefaultExceptionResolver.java index 097e06a1..7e8245cb 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/DefaultExceptionResolver.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/DefaultExceptionResolver.java @@ -40,6 +40,10 @@ @ControllerAdvice public class DefaultExceptionResolver { + private static final String LOGIN_PASS_EMPTY = "login.password.empty"; + private static final String LOGIN_PASS_LONG = "login.password.long"; + private static final String LOGIN_USERNAME_LONG = "login.username.long"; + private static final Logger logger = LoggerFactory.getLogger(DefaultExceptionResolver.class); /** @@ -63,55 +67,28 @@ public class DefaultExceptionResolver { @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public @ResponseBody ErrorResponse handleDefaultException(MethodArgumentNotValidException ex) { + logger.error("Method argument validation failed", ex); List errorMessages = new ArrayList<>(); final List allErrors = ex.getBindingResult().getAllErrors(); - for (ObjectError objError: allErrors) { - if (objError.getCodes() != null){ - errorMessages.addAll(Arrays.asList(objError.getCodes())); - } - } + allErrors.stream() + .filter(objError -> (objError.getCodes() != null)) + .forEachOrdered(objError -> + errorMessages.addAll(Arrays.asList(objError.getCodes())) + ); // preparation of user friendly error messages for the UI String message; if (errorMessages.contains("login.username.empty")) { - if (errorMessages.contains("login.password.empty")) { - message = "login.username.empty login.password.empty"; - } else { - if (errorMessages.contains("login.password.long")) { - message = "login.username.empty login.password.long"; - } else { - message = "login.username.empty"; - } - } + message = processErrorMessagesWhenUsernameEmpty(errorMessages); } else { - if (errorMessages.contains("login.password.empty")) { - if (errorMessages.contains("login.username.long")) { - message = "login.password.empty login.username.long"; - } else { - message = "login.password.empty"; - } - } else { - if (errorMessages.contains("login.username.long")) { - if (errorMessages.contains("login.password.long")) { - message = "login.username.long login.password.long"; - } else { - message = "login.username.long"; - } - } else { - if (errorMessages.contains("login.password.long")) { - message = "login.password.long"; - } else { - message = "login.authenticationFailed"; - } - } - } + message = processErrorMessagesWhenUsernameFilled(errorMessages); } DataAdapterError error = new DataAdapterError(DataAdapterError.Code.INPUT_INVALID, message); error.setValidationErrors(errorMessages); return new ErrorResponse(error); } - + /** * Handling of user not found exception. * @param ex Exception. @@ -120,6 +97,7 @@ public class DefaultExceptionResolver { @ExceptionHandler(UserNotFoundException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public @ResponseBody ErrorResponse handleUserNotFoundException(UserNotFoundException ex) { + logger.debug("User not found", ex); DataAdapterError error = new DataAdapterError(DataAdapterError.Code.USER_NOT_FOUND, ex.getMessage()); return new ErrorResponse(error); } @@ -132,6 +110,7 @@ public class DefaultExceptionResolver { @ExceptionHandler(InvalidOperationContextException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public @ResponseBody ErrorResponse handleInvalidOperationContextException(InvalidOperationContextException ex) { + logger.error("Invalid operation context", ex); DataAdapterError error = new DataAdapterError(DataAdapterError.Code.OPERATION_CONTEXT_INVALID, ex.getMessage()); return new ErrorResponse(error); } @@ -144,6 +123,7 @@ public class DefaultExceptionResolver { @ExceptionHandler(InvalidConsentDataException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public @ResponseBody ErrorResponse handleInvalidConsentException(InvalidConsentDataException ex) { + logger.error("Invalid consent data", ex); DataAdapterError error = new DataAdapterError(DataAdapterError.Code.CONSENT_DATA_INVALID, ex.getMessage()); return new ErrorResponse(error); } @@ -161,4 +141,47 @@ public class DefaultExceptionResolver { return new ErrorResponse(error); } + private String processErrorMessagesWhenUsernameEmpty(List errorMessages) { + if (errorMessages.contains(LOGIN_PASS_EMPTY)) { + return "login.username.empty login.password.empty"; + } else { + if (errorMessages.contains(LOGIN_PASS_LONG)) { + return "login.username.empty login.password.long"; + } else { + return "login.username.empty"; + } + } + } + + private String processErrorMessagesWhenUsernameFilled(List errorMessages) { + if (errorMessages.contains(LOGIN_PASS_EMPTY)) { + return processErrorMessagesWhenLoginPasswordEmpty(errorMessages); + } else { + return processErrorMessagesWhenLoginPasswordFilled(errorMessages); + } + } + + private String processErrorMessagesWhenLoginPasswordEmpty(List errorMessages) { + if (errorMessages.contains(LOGIN_USERNAME_LONG)) { + return "login.password.empty login.username.long"; + } else { + return LOGIN_PASS_EMPTY; + } + } + + private String processErrorMessagesWhenLoginPasswordFilled(List errorMessages) { + if (errorMessages.contains(LOGIN_USERNAME_LONG)) { + if (errorMessages.contains(LOGIN_PASS_LONG)) { + return "login.username.long login.password.long"; + } else { + return LOGIN_USERNAME_LONG; + } + } else { + if (errorMessages.contains(LOGIN_PASS_LONG)) { + return LOGIN_PASS_LONG; + } else { + return "login.authenticationFailed"; + } + } + } } \ No newline at end of file diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java index ac188805..90e2c134 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java @@ -38,6 +38,11 @@ @Component public class AuthenticationRequestValidator implements Validator { +ยง private static final String OPERATION_CONTEXT_FIELD = "requestObject.operationContext"; + private static final String MISSING_OPERATION_CONTEXT_ERROR_CODE = "operationContext.missing"; + private static final String PASS_FIELD = "requestObject.password"; + private static final String ORGANIZATION_ID_FIELD = "requestObject.organizationId"; + /** * Return whether validator can validate given class. * @param clazz Validated class. @@ -57,67 +62,74 @@ public boolean supports(@NonNull Class clazz) { public void validate(@Nullable Object o, @NonNull Errors errors) { ObjectRequest objectRequest = (ObjectRequest) o; if (objectRequest == null) { - errors.rejectValue("requestObject.operationContext", "operationContext.missing"); + errors.rejectValue(OPERATION_CONTEXT_FIELD, MISSING_OPERATION_CONTEXT_ERROR_CODE); return; } - if (objectRequest.getRequestObject() instanceof UserLookupRequest) { - UserLookupRequest authRequest = (UserLookupRequest) objectRequest.getRequestObject(); + validateUserLookupRequest(objectRequest, errors); + } else if (objectRequest.getRequestObject() instanceof UserAuthenticationRequest) { + validateUserAuthenticationRequest(objectRequest, errors); + } + } + + private void validateUserLookupRequest(ObjectRequest objectRequest, Errors errors) { + UserLookupRequest authRequest = (UserLookupRequest) objectRequest.getRequestObject(); - // update validation logic based on the real Data Adapter requirements - String username = authRequest.getUsername(); - String organizationId = authRequest.getOrganizationId(); - OperationContext operationContext = authRequest.getOperationContext(); - if (operationContext == null) { - errors.rejectValue("requestObject.operationContext", "operationContext.missing"); - } - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.username", "login.username.empty"); - if (username != null && username.length() > 30) { - errors.rejectValue("requestObject.username", "login.username.long"); - } + // update validation logic based on the real Data Adapter requirements + String username = authRequest.getUsername(); + String organizationId = authRequest.getOrganizationId(); + OperationContext operationContext = authRequest.getOperationContext(); + if (operationContext == null) { + errors.rejectValue(OPERATION_CONTEXT_FIELD, MISSING_OPERATION_CONTEXT_ERROR_CODE); + } + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.username", "login.username.empty"); + if (username != null && username.length() > 30) { + errors.rejectValue("requestObject.username", "login.username.long"); + } - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.organizationId", "login.organizationId.empty"); - if (organizationId != null && organizationId.length() > 256) { - errors.rejectValue("requestObject.organizationId", "login.organizationId.long"); - } - } else if (objectRequest.getRequestObject() instanceof UserAuthenticationRequest) { - UserAuthenticationRequest authRequest = (UserAuthenticationRequest) objectRequest.getRequestObject(); + ValidationUtils.rejectIfEmptyOrWhitespace(errors, ORGANIZATION_ID_FIELD, "login.organizationId.empty"); + if (organizationId != null && organizationId.length() > 256) { + errors.rejectValue(ORGANIZATION_ID_FIELD, "login.organizationId.long"); + } + } + + private void validateUserAuthenticationRequest(ObjectRequest objectRequest, Errors errors) { + UserAuthenticationRequest authRequest = (UserAuthenticationRequest) objectRequest.getRequestObject(); - // update validation logic based on the real Data Adapter requirements - String userId = authRequest.getUserId(); - String password = authRequest.getPassword(); - String organizationId = authRequest.getOrganizationId(); - OperationContext operationContext = authRequest.getOperationContext(); - if (operationContext == null) { - errors.rejectValue("requestObject.operationContext", "operationContext.missing"); - } - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.userId", "login.userId.empty"); - if (userId != null && userId.length() > 30) { - errors.rejectValue("requestObject.userId", "login.userId.long"); - } + // update validation logic based on the real Data Adapter requirements + String userId = authRequest.getUserId(); + String password = authRequest.getPassword(); + String organizationId = authRequest.getOrganizationId(); + OperationContext operationContext = authRequest.getOperationContext(); + if (operationContext == null) { + errors.rejectValue(OPERATION_CONTEXT_FIELD, MISSING_OPERATION_CONTEXT_ERROR_CODE); + } + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.userId", "login.userId.empty"); + if (userId != null && userId.length() > 30) { + errors.rejectValue("requestObject.userId", "login.userId.long"); + } - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.password", "login.password.empty"); - AuthenticationContext authenticationContext = authRequest.getAuthenticationContext(); - PasswordProtectionType passwordProtection = authenticationContext.getPasswordProtection(); - if (passwordProtection == PasswordProtectionType.NO_PROTECTION) { - if (password != null && password.length() > 30) { - errors.rejectValue("requestObject.password", "login.password.long"); - } - } else { - // Allow longer values in password field when password is encrypted - if (password != null && password.length() > 256) { - errors.rejectValue("requestObject.password", "login.password.long"); - } + ValidationUtils.rejectIfEmptyOrWhitespace(errors, PASS_FIELD, "login.password.empty"); + AuthenticationContext authenticationContext = authRequest.getAuthenticationContext(); + PasswordProtectionType passwordProtection = authenticationContext.getPasswordProtection(); + if (passwordProtection == PasswordProtectionType.NO_PROTECTION) { + if (password != null && password.length() > 30) { + errors.rejectValue(PASS_FIELD, "login.password.long"); } - - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.organizationId", "login.organizationId.empty"); - if (userId != null && organizationId.length() > 256) { - errors.rejectValue("requestObject.organizationId", "login.organizationId.long"); + } else { + // Allow longer values in password field when password is encrypted + if (password != null && password.length() > 256) { + errors.rejectValue(PASS_FIELD, "login.password.long"); } + } - if (passwordProtection != PasswordProtectionType.NO_PROTECTION && passwordProtection != PasswordProtectionType.PASSWORD_ENCRYPTION_AES) { - errors.rejectValue("requestObject.authenticationContext", "login.type.unsupported"); - } + ValidationUtils.rejectIfEmptyOrWhitespace(errors, ORGANIZATION_ID_FIELD, "login.organizationId.empty"); + if (userId != null && organizationId.length() > 256) { + errors.rejectValue(ORGANIZATION_ID_FIELD, "login.organizationId.long"); + } + + if (passwordProtection != PasswordProtectionType.NO_PROTECTION && passwordProtection != PasswordProtectionType.PASSWORD_ENCRYPTION_AES) { + errors.rejectValue("requestObject.authenticationContext", "login.type.unsupported"); } } } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/ConsentFormRequestValidator.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/ConsentFormRequestValidator.java index d7ad2828..78729394 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/ConsentFormRequestValidator.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/ConsentFormRequestValidator.java @@ -41,6 +41,8 @@ @Component public class ConsentFormRequestValidator implements Validator { + private static final String INVALID_REQUEST_MESSAGE = "consent.invalidRequest"; + /** * Return whether validator can validate given class. * @param clazz Validated class. @@ -82,7 +84,7 @@ public void validate(@Nullable Object o, @NonNull Errors errors) { if (request.getOperationContext() != null) { validateOperationName(request.getOperationContext().getName(), errors); } - validateLanguage(errors); + validateLanguage(request.getLang(), errors); } else if (objectRequest.getRequestObject() instanceof ValidateConsentFormRequest) { ObjectRequest requestObject = (ObjectRequest) o; ValidateConsentFormRequest request = requestObject.getRequestObject(); @@ -91,7 +93,7 @@ public void validate(@Nullable Object o, @NonNull Errors errors) { if (request.getOperationContext() != null) { validateOperationName(request.getOperationContext().getName(), errors); } - validateLanguage(errors); + validateLanguage(request.getLang(), errors); validateOptions(request.getOptions(), errors); } else if (objectRequest.getRequestObject() instanceof SaveConsentFormRequest) { ObjectRequest requestObject = (ObjectRequest) o; @@ -112,28 +114,30 @@ private void validateOperationContext(OperationContext operationContext, Errors } private void validateUserId(String userId, Errors errors) { - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.userId", "consent.invalidRequest"); + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.userId", INVALID_REQUEST_MESSAGE); if (userId != null && userId.length() > 30) { - errors.rejectValue("requestObject.userId", "consent.invalidRequest"); + errors.rejectValue("requestObject.userId", INVALID_REQUEST_MESSAGE); } } private void validateOperationName(String operationName, Errors errors) { - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.operationContext.name", "consent.invalidRequest"); + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.operationContext.name", INVALID_REQUEST_MESSAGE); if (operationName != null && operationName.length() > 32) { - errors.rejectValue("requestObject.operationContext.name", "consent.invalidRequest"); + errors.rejectValue("requestObject.operationContext.name", INVALID_REQUEST_MESSAGE); } } - private void validateLanguage(Errors errors) { - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.lang", "consent.invalidRequest"); - // Do not validate lang parameter, fallback to "en" instead, see: https://github.com/wultra/powerauth-webflow-customization/issues/104 + private void validateLanguage(String lang, Errors errors) { + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.lang", INVALID_REQUEST_MESSAGE); + if (lang != null && !lang.equals("cs") && !lang.equals("en")) { + errors.rejectValue("requestObject.lang", INVALID_REQUEST_MESSAGE); + } } private void validateOptions(List options, Errors errors) { - ValidationUtils.rejectIfEmpty(errors, "requestObject.options", "consent.invalidRequest"); - if (options != null && options.isEmpty()) { - errors.rejectValue("requestObject.options", "consent.invalidRequest"); + // Allow empty options, but do not allow null value + if (options == null) { + errors.rejectValue("requestObject.options", INVALID_REQUEST_MESSAGE); } } } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSmsAuthorizationRequestValidator.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSmsAuthorizationRequestValidator.java index 62f523a9..2c9035b7 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSmsAuthorizationRequestValidator.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSmsAuthorizationRequestValidator.java @@ -41,7 +41,10 @@ @Component public class CreateSmsAuthorizationRequestValidator implements Validator { - private OperationValueExtractionService operationValueExtractionService; + private static final String OPERATION_CONTEXT_FIELD = "requestObject.operationContext"; + private static final String AMOUNT_EMPTY_ERROR_CODE = "smsAuthorization.amount.empty"; + + private final OperationValueExtractionService operationValueExtractionService; /** * Validator constructor. @@ -72,7 +75,7 @@ public boolean supports(@NonNull Class clazz) { public void validate(@Nullable Object o, @NonNull Errors errors) { ObjectRequest requestObject = (ObjectRequest) o; if (requestObject == null) { - errors.rejectValue("requestObject.operationContext", "operationContext.missing"); + errors.rejectValue(OPERATION_CONTEXT_FIELD, "operationContext.missing"); return; } CreateSmsAuthorizationRequest authRequest = requestObject.getRequestObject(); @@ -82,7 +85,7 @@ public void validate(@Nullable Object o, @NonNull Errors errors) { String organizationId = authRequest.getOrganizationId(); OperationContext operationContext = authRequest.getOperationContext(); if (operationContext == null) { - errors.rejectValue("requestObject.operationContext", "operationContext.missing"); + errors.rejectValue(OPERATION_CONTEXT_FIELD, "operationContext.missing"); } String operationName = authRequest.getOperationContext().getName(); @@ -102,49 +105,53 @@ public void validate(@Nullable Object o, @NonNull Errors errors) { errors.rejectValue("requestObject.operationContext.name", "smsAuthorization.operationName.long"); } - if (operationName != null) { - switch (operationName) { - case "login": - case "login_sca": - // no field validation required - break; - case "authorize_payment": - case "authorize_payment_sca": - AmountAttribute amountAttribute; - try { - amountAttribute = operationValueExtractionService.getAmount(authRequest.getOperationContext()); - if (amountAttribute == null) { - errors.rejectValue("requestObject.operationContext", "smsAuthorization.amount.empty"); - } else { - BigDecimal amount = amountAttribute.getAmount(); - String currency = amountAttribute.getCurrency(); + if (operationName == null) { + return; + } + + switch (operationName) { + case "login": + // no field validation required + break; + case "authorize_payment": + validateFieldsForPayment(authRequest, errors); + break; + default: + throw new IllegalStateException("Unsupported operation in validator: " + operationName); + } + } + + private void validateFieldsForPayment(CreateSmsAuthorizationRequest authRequest, Errors errors) { + AmountAttribute amountAttribute; + try { + amountAttribute = operationValueExtractionService.getAmount(authRequest.getOperationContext()); + if (amountAttribute == null) { + errors.rejectValue(OPERATION_CONTEXT_FIELD, AMOUNT_EMPTY_ERROR_CODE); + } else { + BigDecimal amount = amountAttribute.getAmount(); + String currency = amountAttribute.getCurrency(); - if (amount == null) { - errors.rejectValue("requestObject.operationContext", "smsAuthorization.amount.empty"); - } else if (amount.doubleValue() <= 0) { - errors.rejectValue("requestObject.operationContext", "smsAuthorization.amount.invalid"); - } + if (amount == null) { + errors.rejectValue(OPERATION_CONTEXT_FIELD, AMOUNT_EMPTY_ERROR_CODE); + } else if (amount.doubleValue() <= 0) { + errors.rejectValue(OPERATION_CONTEXT_FIELD, "smsAuthorization.amount.invalid"); + } - if (currency == null || currency.isEmpty()) { - errors.rejectValue("requestObject.operationContext", "smsAuthorization.currency.empty"); - } - } - } catch (InvalidOperationContextException ex) { - errors.rejectValue("requestObject.operationContext", "smsAuthorization.amount.empty"); - } - String account; - try { - account = operationValueExtractionService.getAccount(authRequest.getOperationContext()); - if (account == null || account.isEmpty()) { - errors.rejectValue("requestObject.operationContext", "smsAuthorization.account.empty"); - } - } catch (InvalidOperationContextException ex) { - errors.rejectValue("requestObject.operationContext", "smsAuthorization.account.empty"); - } - break; - default: - throw new IllegalStateException("Unsupported operation in validator: " + operationName); + if (currency == null || currency.isEmpty()) { + errors.rejectValue(OPERATION_CONTEXT_FIELD, "smsAuthorization.currency.empty"); + } + } + } catch (InvalidOperationContextException ex) { + errors.rejectValue(OPERATION_CONTEXT_FIELD, AMOUNT_EMPTY_ERROR_CODE); + } + String account; + try { + account = operationValueExtractionService.getAccount(authRequest.getOperationContext()); + if (account == null || account.isEmpty()) { + errors.rejectValue(OPERATION_CONTEXT_FIELD, "smsAuthorization.account.empty"); } + } catch (InvalidOperationContextException ex) { + errors.rejectValue(OPERATION_CONTEXT_FIELD, "smsAuthorization.account.empty"); } } } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SmsPersistenceService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SmsPersistenceService.java index b3aca997..45a7c138 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SmsPersistenceService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SmsPersistenceService.java @@ -16,7 +16,6 @@ package io.getlime.security.powerauth.app.dataadapter.service; import io.getlime.security.powerauth.app.dataadapter.configuration.DataAdapterConfiguration; -import io.getlime.security.powerauth.app.dataadapter.exception.InvalidOperationContextException; import io.getlime.security.powerauth.app.dataadapter.repository.SmsAuthorizationRepository; import io.getlime.security.powerauth.app.dataadapter.repository.model.entity.SmsAuthorizationEntity; import io.getlime.security.powerauth.lib.dataadapter.model.entity.AuthorizationCode; From 1507863bc33cd9b2e48ddfd3469b2afd006c7a3e Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Thu, 10 Oct 2019 13:29:02 +0200 Subject: [PATCH 61/79] Remove invalid character --- .../impl/validation/AuthenticationRequestValidator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java index 90e2c134..3cb450a8 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java @@ -38,7 +38,7 @@ @Component public class AuthenticationRequestValidator implements Validator { -ยง private static final String OPERATION_CONTEXT_FIELD = "requestObject.operationContext"; + private static final String OPERATION_CONTEXT_FIELD = "requestObject.operationContext"; private static final String MISSING_OPERATION_CONTEXT_ERROR_CODE = "operationContext.missing"; private static final String PASS_FIELD = "requestObject.password"; private static final String ORGANIZATION_ID_FIELD = "requestObject.organizationId"; From 7f0fb128292a99966a16abce1695a53898fd930f Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Thu, 10 Oct 2019 14:07:36 +0200 Subject: [PATCH 62/79] Revert unwanted changes --- .../impl/validation/ConsentFormRequestValidator.java | 10 ++++------ .../CreateSmsAuthorizationRequestValidator.java | 2 ++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/ConsentFormRequestValidator.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/ConsentFormRequestValidator.java index 78729394..76002fc8 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/ConsentFormRequestValidator.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/ConsentFormRequestValidator.java @@ -84,7 +84,7 @@ public void validate(@Nullable Object o, @NonNull Errors errors) { if (request.getOperationContext() != null) { validateOperationName(request.getOperationContext().getName(), errors); } - validateLanguage(request.getLang(), errors); + validateLanguage(errors); } else if (objectRequest.getRequestObject() instanceof ValidateConsentFormRequest) { ObjectRequest requestObject = (ObjectRequest) o; ValidateConsentFormRequest request = requestObject.getRequestObject(); @@ -93,7 +93,7 @@ public void validate(@Nullable Object o, @NonNull Errors errors) { if (request.getOperationContext() != null) { validateOperationName(request.getOperationContext().getName(), errors); } - validateLanguage(request.getLang(), errors); + validateLanguage(errors); validateOptions(request.getOptions(), errors); } else if (objectRequest.getRequestObject() instanceof SaveConsentFormRequest) { ObjectRequest requestObject = (ObjectRequest) o; @@ -127,11 +127,9 @@ private void validateOperationName(String operationName, Errors errors) { } } - private void validateLanguage(String lang, Errors errors) { + private void validateLanguage(Errors errors) { ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.lang", INVALID_REQUEST_MESSAGE); - if (lang != null && !lang.equals("cs") && !lang.equals("en")) { - errors.rejectValue("requestObject.lang", INVALID_REQUEST_MESSAGE); - } + // Do not validate lang parameter, fallback to "en" instead, see: https://github.com/wultra/powerauth-webflow-customization/issues/104 } private void validateOptions(List options, Errors errors) { diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSmsAuthorizationRequestValidator.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSmsAuthorizationRequestValidator.java index 2c9035b7..24e36f9a 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSmsAuthorizationRequestValidator.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSmsAuthorizationRequestValidator.java @@ -111,9 +111,11 @@ public void validate(@Nullable Object o, @NonNull Errors errors) { switch (operationName) { case "login": + case "login_sca": // no field validation required break; case "authorize_payment": + case "authorize_payment_sca": validateFieldsForPayment(authRequest, errors); break; default: From 55b8cf726cfc54759437f130163573695c68f5b0 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Fri, 11 Oct 2019 18:20:24 +0200 Subject: [PATCH 63/79] Fix #110: Update sample implementation with user account status --- .../powerauth/app/dataadapter/api/DataAdapter.java | 5 +++-- .../controller/SmsAuthorizationController.java | 4 +++- .../app/dataadapter/impl/service/DataAdapterService.java | 8 +++++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java index 8bbaa74a..feee3e41 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java @@ -17,7 +17,7 @@ import io.getlime.security.powerauth.app.dataadapter.exception.*; import io.getlime.security.powerauth.lib.dataadapter.model.entity.*; -import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.AuthInstrument; +import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.AccountStatus; import io.getlime.security.powerauth.lib.dataadapter.model.request.AfsRequestParameters; import io.getlime.security.powerauth.lib.dataadapter.model.response.*; @@ -100,13 +100,14 @@ public interface DataAdapter { * Create authorization SMS message and send it. * @param userId User ID. * @param organizationId Organization ID. + * @param accountStatus User account status. * @param operationContext Operation context. * @param lang Language for localization. * @return Message ID. * @throws InvalidOperationContextException Thrown when operation context is invalid. * @throws DataAdapterRemoteException Thrown when remote communication fails or SMS message could not be delivered. */ - CreateSmsAuthorizationResponse createAndSendAuthorizationSms(String userId, String organizationId, OperationContext operationContext, String lang) throws InvalidOperationContextException, DataAdapterRemoteException; + CreateSmsAuthorizationResponse createAndSendAuthorizationSms(String userId, String organizationId, AccountStatus accountStatus, OperationContext operationContext, String lang) throws InvalidOperationContextException, DataAdapterRemoteException; /** * Verify authorization code from SMS message. diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java index fb4d881c..73f4f6da 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java @@ -23,6 +23,7 @@ import io.getlime.security.powerauth.app.dataadapter.impl.validation.CreateSmsAuthorizationRequestValidator; import io.getlime.security.powerauth.lib.dataadapter.model.entity.AuthenticationContext; import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; +import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.AccountStatus; import io.getlime.security.powerauth.lib.dataadapter.model.request.CreateSmsAuthorizationRequest; import io.getlime.security.powerauth.lib.dataadapter.model.request.VerifySmsAndPasswordRequest; import io.getlime.security.powerauth.lib.dataadapter.model.request.VerifySmsAuthorizationRequest; @@ -87,9 +88,10 @@ public ObjectResponse createAuthorizationSms(@Va // Create authorization SMS and persist it. String userId = smsRequest.getUserId(); String organizationId = smsRequest.getOrganizationId(); + AccountStatus accountStatus = smsRequest.getAccountStatus(); OperationContext operationContext = smsRequest.getOperationContext(); String lang = smsRequest.getLang(); - CreateSmsAuthorizationResponse response = dataAdapter.createAndSendAuthorizationSms(userId, organizationId, operationContext, lang); + CreateSmsAuthorizationResponse response = dataAdapter.createAndSendAuthorizationSms(userId, organizationId, accountStatus, operationContext, lang); logger.info("The createAuthorizationSms request succeeded, operation ID: {}", request.getRequestObject().getOperationContext().getId()); return new ObjectResponse<>(response); diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index fcd4aba4..2c64ce1c 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -51,7 +51,8 @@ public DataAdapterService(DataAdapterI18NService dataAdapterI18NService, SmsPers @Override public UserDetailResponse lookupUser(String username, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, UserNotFoundException { // The sample Data Adapter code uses 1:1 mapping of username to userId. In real implementation the userId usually differs from the username, so translation of username to user ID is required. - // If user does not exist or user account is blocked and such error needs to be silent, return null values for user ID and organization ID. + // If the user does not exist, return null values for user ID and organization ID. + // If user account account is blocked, return AccountStatus.NOT_ACTIVE as account status. // The SCA login fakes SMS message delivery even for case when user ID is null to disallow fishing of usernames. // For case when an error should appear instead, throw a UserNotFoundException. return fetchUserDetail(username, organizationId, operationContext); @@ -105,6 +106,7 @@ public UserDetailResponse fetchUserDetail(String userId, String organizationId, responseObject.setGivenName("John"); responseObject.setFamilyName("Doe"); responseObject.setOrganizationId(organizationId); + responseObject.setAccountStatus(AccountStatus.ACTIVE); return responseObject; } @@ -195,14 +197,14 @@ public void operationChangedNotification(String userId, String organizationId, O } @Override - public CreateSmsAuthorizationResponse createAndSendAuthorizationSms(String userId, String organizationId, OperationContext operationContext, String lang) throws InvalidOperationContextException, DataAdapterRemoteException { + public CreateSmsAuthorizationResponse createAndSendAuthorizationSms(String userId, String organizationId, AccountStatus accountStatus, OperationContext operationContext, String lang) throws InvalidOperationContextException, DataAdapterRemoteException { CreateSmsAuthorizationResponse response = new CreateSmsAuthorizationResponse(); // MessageId is generated as random UUID, it can be overridden to provide a real message identification String messageId = UUID.randomUUID().toString(); response.setMessageId(messageId); // Fake SMS message delivery for null user ID in case of non-existent account or blocked user account - if (userId == null) { + if (userId == null || accountStatus != AccountStatus.ACTIVE) { // Make sure that user cannot recognize that the SMS was not sent, even the result is sent as fake success response.setSmsDeliveryResult(SmsDeliveryResult.SUCCEEDED); return response; From 8b92b6e095a05c971e6ba4d9434b1a3ccd14c156 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Tue, 15 Oct 2019 21:33:16 +0200 Subject: [PATCH 64/79] Improve enumerations for authentication results --- .../app/dataadapter/controller/AfsController.java | 2 -- .../impl/service/DataAdapterService.java | 10 +++++----- .../dataadapter/service/SmsPersistenceService.java | 14 +++++++------- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AfsController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AfsController.java index 6c6e26a8..912554fc 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AfsController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AfsController.java @@ -21,7 +21,6 @@ import io.getlime.security.powerauth.app.dataadapter.exception.DataAdapterRemoteException; import io.getlime.security.powerauth.app.dataadapter.exception.InvalidOperationContextException; import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; -import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.AuthInstrument; import io.getlime.security.powerauth.lib.dataadapter.model.request.*; import io.getlime.security.powerauth.lib.dataadapter.model.response.*; import org.slf4j.Logger; @@ -29,7 +28,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; -import java.util.List; import java.util.Map; /** diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index 2c64ce1c..1ede5f79 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -73,15 +73,15 @@ public UserAuthenticationResponse authenticateUser(String userId, String passwor // The organization needs to be set in response (e.g. client authenticated against RETAIL organization or SME organization). userDetail.setOrganizationId(organizationId); authResponse.setUserDetail(userDetail); - authResponse.setAuthenticationResult(UserAuthenticationResult.VERIFIED_SUCCEEDED); + authResponse.setAuthenticationResult(UserAuthenticationResult.SUCCEEDED); return authResponse; } catch (UserNotFoundException e) { - authResponse.setAuthenticationResult(UserAuthenticationResult.VERIFIED_FAILED); + authResponse.setAuthenticationResult(UserAuthenticationResult.FAILED); authResponse.setErrorMessage(AUTHENTICATION_FAILED); return authResponse; } } - authResponse.setAuthenticationResult(UserAuthenticationResult.VERIFIED_FAILED); + authResponse.setAuthenticationResult(UserAuthenticationResult.FAILED); authResponse.setErrorMessage(AUTHENTICATION_FAILED); // Set number of remaining attempts for this user ID in case it is available. // authResponse.setRemainingAttempts(5); @@ -255,8 +255,8 @@ public VerifySmsAndPasswordResponse verifyAuthorizationSmsAndPassword(String use // Create aggregate response response.setSmsAuthorizationResult(smsResponse.getSmsAuthorizationResult()); response.setUserAuthenticationResult(authResponse.getAuthenticationResult()); - if (smsResponse.getSmsAuthorizationResult() != SmsAuthorizationResult.VERIFIED_SUCCEEDED - || authResponse.getAuthenticationResult() != UserAuthenticationResult.VERIFIED_SUCCEEDED) { + if (smsResponse.getSmsAuthorizationResult() != SmsAuthorizationResult.SUCCEEDED + || authResponse.getAuthenticationResult() != UserAuthenticationResult.SUCCEEDED) { // Provide an error message which does not allow to find out reason of failed verification. response.setErrorMessage(AUTHENTICATION_FAILED); } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SmsPersistenceService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SmsPersistenceService.java index 45a7c138..d28549d5 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SmsPersistenceService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SmsPersistenceService.java @@ -96,7 +96,7 @@ public VerifySmsAuthorizationResponse verifyAuthorizationSms(String messageId, S Optional smsEntityOptional = smsAuthorizationRepository.findById(messageId); VerifySmsAuthorizationResponse response = new VerifySmsAuthorizationResponse(); if (!smsEntityOptional.isPresent()) { - response.setSmsAuthorizationResult(SmsAuthorizationResult.VERIFIED_FAILED); + response.setSmsAuthorizationResult(SmsAuthorizationResult.FAILED); response.setErrorMessage("smsAuthorization.invalidMessage"); return response; } @@ -108,29 +108,29 @@ public VerifySmsAuthorizationResponse verifyAuthorizationSms(String messageId, S final Integer remainingAttempts = dataAdapterConfiguration.getSmsOtpMaxVerifyTriesPerMessage() - smsEntity.getVerifyRequestCount(); if (smsEntity.getAuthorizationCode() == null || smsEntity.getAuthorizationCode().isEmpty()) { - response.setSmsAuthorizationResult(SmsAuthorizationResult.VERIFIED_FAILED); + response.setSmsAuthorizationResult(SmsAuthorizationResult.FAILED); response.setRemainingAttempts(remainingAttempts); response.setErrorMessage("smsAuthorization.invalidCode"); return response; } if (smsEntity.isExpired()) { - response.setSmsAuthorizationResult(SmsAuthorizationResult.VERIFIED_FAILED); + response.setSmsAuthorizationResult(SmsAuthorizationResult.FAILED); response.setErrorMessage("smsAuthorization.expired"); return response; } if (!allowMultipleVerifications && smsEntity.isVerified()) { - response.setSmsAuthorizationResult(SmsAuthorizationResult.VERIFIED_FAILED); + response.setSmsAuthorizationResult(SmsAuthorizationResult.FAILED); response.setErrorMessage("smsAuthorization.alreadyVerified"); return response; } if (smsEntity.getVerifyRequestCount() > dataAdapterConfiguration.getSmsOtpMaxVerifyTriesPerMessage()) { - response.setSmsAuthorizationResult(SmsAuthorizationResult.VERIFIED_FAILED); + response.setSmsAuthorizationResult(SmsAuthorizationResult.FAILED); response.setErrorMessage("smsAuthorization.maxAttemptsExceeded"); return response; } String authorizationCodeExpected = smsEntity.getAuthorizationCode(); if (!authorizationCode.equals(authorizationCodeExpected)) { - response.setSmsAuthorizationResult(SmsAuthorizationResult.VERIFIED_FAILED); + response.setSmsAuthorizationResult(SmsAuthorizationResult.FAILED); response.setRemainingAttempts(remainingAttempts); response.setErrorMessage("smsAuthorization.failed"); return response; @@ -141,7 +141,7 @@ public VerifySmsAuthorizationResponse verifyAuthorizationSms(String messageId, S smsEntity.setTimestampVerified(new Date()); smsAuthorizationRepository.save(smsEntity); - response.setSmsAuthorizationResult(SmsAuthorizationResult.VERIFIED_SUCCEEDED); + response.setSmsAuthorizationResult(SmsAuthorizationResult.SUCCEEDED); return response; } From 50444eb79f14018307f2e1823795911fc94ba72e Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Tue, 22 Oct 2019 18:22:36 +0200 Subject: [PATCH 65/79] Propagate account status into SMS verification --- .../security/powerauth/app/dataadapter/api/DataAdapter.java | 6 ++++-- .../dataadapter/controller/SmsAuthorizationController.java | 6 ++++-- .../app/dataadapter/impl/service/DataAdapterService.java | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java index feee3e41..02a5597e 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java @@ -113,6 +113,7 @@ public interface DataAdapter { * Verify authorization code from SMS message. * @param userId User ID. * @param organizationId Organization ID. + * @param accountStatus Current user account status. * @param messageId Message ID. * @param authorizationCode Authorization code. * @param operationContext Operation context. @@ -120,12 +121,13 @@ public interface DataAdapter { * @throws DataAdapterRemoteException Thrown when remote communication fails. * @throws InvalidOperationContextException Thrown when operation context is invalid. */ - VerifySmsAuthorizationResponse verifyAuthorizationSms(String userId, String organizationId, String messageId, String authorizationCode, OperationContext operationContext) throws DataAdapterRemoteException, InvalidOperationContextException; + VerifySmsAuthorizationResponse verifyAuthorizationSms(String userId, String organizationId, AccountStatus accountStatus, String messageId, String authorizationCode, OperationContext operationContext) throws DataAdapterRemoteException, InvalidOperationContextException; /** * Verify authorization code from SMS message together with user password. * @param userId User ID. * @param organizationId Organization ID. + * @param accountStatus Current user account status. * @param messageId Message ID. * @param authorizationCode Authorization code. * @param operationContext Operation context. @@ -135,7 +137,7 @@ public interface DataAdapter { * @throws DataAdapterRemoteException Thrown when remote communication fails. * @throws InvalidOperationContextException Thrown when operation context is invalid. */ - VerifySmsAndPasswordResponse verifyAuthorizationSmsAndPassword(String userId, String organizationId, String messageId, String authorizationCode, OperationContext operationContext, AuthenticationContext authenticationContext, String password) throws DataAdapterRemoteException, InvalidOperationContextException; + VerifySmsAndPasswordResponse verifyAuthorizationSmsAndPassword(String userId, String organizationId, AccountStatus accountStatus, String messageId, String authorizationCode, OperationContext operationContext, AuthenticationContext authenticationContext, String password) throws DataAdapterRemoteException, InvalidOperationContextException; /** * Decide whether OAuth 2.0 consent form should be displayed based on operation context. diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java index 73f4f6da..47f5dd41 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java @@ -111,11 +111,12 @@ public ObjectResponse verifyAuthorizationSms(@Re VerifySmsAuthorizationRequest verifyRequest = request.getRequestObject(); String userId = verifyRequest.getUserId(); String organizationId = verifyRequest.getOrganizationId(); + AccountStatus accountStatus = verifyRequest.getAccountStatus(); String messageId = verifyRequest.getMessageId(); String authorizationCode = verifyRequest.getAuthorizationCode(); OperationContext operationContext = verifyRequest.getOperationContext(); // Verify authorization code - VerifySmsAuthorizationResponse response = dataAdapter.verifyAuthorizationSms(userId, organizationId, messageId, authorizationCode, operationContext); + VerifySmsAuthorizationResponse response = dataAdapter.verifyAuthorizationSms(userId, organizationId, accountStatus, messageId, authorizationCode, operationContext); logger.info("The verifyAuthorizationSms request succeeded, operation ID: {}", request.getRequestObject().getOperationContext().getId()); return new ObjectResponse<>(response); } @@ -134,12 +135,13 @@ public ObjectResponse verifyAuthorizationSmsAndPas VerifySmsAndPasswordRequest verifyRequest = request.getRequestObject(); String userId = verifyRequest.getUserId(); String organizationId = verifyRequest.getOrganizationId(); + AccountStatus accountStatus = verifyRequest.getAccountStatus(); String messageId = verifyRequest.getMessageId(); String authorizationCode = verifyRequest.getAuthorizationCode(); OperationContext operationContext = verifyRequest.getOperationContext(); String password = verifyRequest.getPassword(); AuthenticationContext authenticationContext = verifyRequest.getAuthenticationContext(); - VerifySmsAndPasswordResponse response = dataAdapter.verifyAuthorizationSmsAndPassword(userId, organizationId, messageId, authorizationCode, operationContext, authenticationContext, password); + VerifySmsAndPasswordResponse response = dataAdapter.verifyAuthorizationSmsAndPassword(userId, organizationId, accountStatus, messageId, authorizationCode, operationContext, authenticationContext, password); logger.info("The verifyAuthorizationSmsAndPassword request succeeded, operation ID: {}", request.getRequestObject().getOperationContext().getId()); return new ObjectResponse<>(response); } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index 1ede5f79..7ae03798 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -231,7 +231,7 @@ public CreateSmsAuthorizationResponse createAndSendAuthorizationSms(String userI } @Override - public VerifySmsAuthorizationResponse verifyAuthorizationSms(String userId, String organizationId, String messageId, String authorizationCode, OperationContext operationContext) throws DataAdapterRemoteException, InvalidOperationContextException { + public VerifySmsAuthorizationResponse verifyAuthorizationSms(String userId, String organizationId, AccountStatus accountStatus, String messageId, String authorizationCode, OperationContext operationContext) throws DataAdapterRemoteException, InvalidOperationContextException { // You can override this logic in case more complex handling of SMS verification is required. VerifySmsAuthorizationResponse response = smsPersistenceService.verifyAuthorizationSms(messageId, authorizationCode, false); // Set number of remaining attempts for verification in case it is available. @@ -242,7 +242,7 @@ public VerifySmsAuthorizationResponse verifyAuthorizationSms(String userId, Stri } @Override - public VerifySmsAndPasswordResponse verifyAuthorizationSmsAndPassword(String userId, String organizationId, String messageId, String authorizationCode, OperationContext operationContext, AuthenticationContext authenticationContext, String password) throws DataAdapterRemoteException, InvalidOperationContextException { + public VerifySmsAndPasswordResponse verifyAuthorizationSmsAndPassword(String userId, String organizationId, AccountStatus accountStatus, String messageId, String authorizationCode, OperationContext operationContext, AuthenticationContext authenticationContext, String password) throws DataAdapterRemoteException, InvalidOperationContextException { VerifySmsAndPasswordResponse response = new VerifySmsAndPasswordResponse(); // Verify authorization code from SMS From 20a18acae1b5dea586a179266c84bfeb808666fc Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Tue, 22 Oct 2019 18:55:51 +0200 Subject: [PATCH 66/79] Validate AFS parameters in default implementation --- .../app/dataadapter/controller/AfsController.java | 2 -- .../app/dataadapter/impl/service/DataAdapterService.java | 7 +++++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AfsController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AfsController.java index 912554fc..11474aef 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AfsController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AfsController.java @@ -52,8 +52,6 @@ public AfsController(DataAdapter dataAdapter) { this.dataAdapter = dataAdapter; } - // TODO - implement validator - /** * Execute an anti-fraud system action and return response for usage in Web Flow. * @param request AFS request. diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index 7ae03798..66e10554 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -36,6 +36,7 @@ public class DataAdapterService implements DataAdapter { private static final String AUTHENTICATION_FAILED = "login.authenticationFailed"; private static final String SMS_DELIVERY_FAILED = "smsAuthorization.deliveryFailed"; private static final String SMS_AUTHORIZATION_FAILED = "smsAuthorization.failed"; + private static final String INVALID_REQUEST = "error.invalidRequest"; private final DataAdapterI18NService dataAdapterI18NService; private final SmsPersistenceService smsPersistenceService; @@ -435,6 +436,12 @@ public SaveConsentFormResponse saveConsentForm(String userId, String organizatio @Override public AfsResponse executeAfsAction(String userId, String organizationId, OperationContext operationContext, AfsRequestParameters afsRequestParameters, Map extras) throws DataAdapterRemoteException, InvalidOperationContextException { + if (userId == null || organizationId == null || operationContext == null || afsRequestParameters == null + || afsRequestParameters.getAfsAction() == null || afsRequestParameters.getAfsType() == null) { + logger.warn("Invalid AFS request received"); + throw new InvalidOperationContextException(INVALID_REQUEST); + } + // Call anti-fraud system and return response for Web Flow. In default implementation of Data Adapter // a mocked response is returned with static 2FA AFS label except for the case of payment with low amount. AfsResponse response = new AfsResponse(); From 3a7abd3e587f79c947d9d749c1fbbd970c0bb521 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Tue, 22 Oct 2019 19:00:20 +0200 Subject: [PATCH 67/79] Update dependencies --- powerauth-data-adapter/pom.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/powerauth-data-adapter/pom.xml b/powerauth-data-adapter/pom.xml index b4fd6826..bee456c3 100644 --- a/powerauth-data-adapter/pom.xml +++ b/powerauth-data-adapter/pom.xml @@ -14,7 +14,7 @@ org.springframework.boot spring-boot-starter-parent - 2.1.6.RELEASE + 2.1.9.RELEASE @@ -101,12 +101,12 @@ com.fasterxml.jackson.datatype jackson-datatype-joda - 2.10.0.pr1 + 2.10.0 org.bouncycastle bcprov-jdk15on - 1.61 + 1.64 provided @@ -137,7 +137,7 @@ com.google.guava guava - 27.0.1-jre + 28.1-jre From f244380a4e145649e1a5bb102c9b8cdc7d1e5c47 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Fri, 1 Nov 2019 11:03:00 -0400 Subject: [PATCH 68/79] Unify endpoint naming conventions for AFS with other endpoints --- .../powerauth/app/dataadapter/controller/AfsController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AfsController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AfsController.java index 11474aef..6e2fb56d 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AfsController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AfsController.java @@ -36,7 +36,7 @@ * @author Roman Strobl, roman.strobl@wultra.com */ @RestController -@RequestMapping("/api/afs") +@RequestMapping("/api/afs/action") public class AfsController { private static final Logger logger = LoggerFactory.getLogger(AfsController.class); @@ -59,7 +59,7 @@ public AfsController(DataAdapter dataAdapter) { * @throws DataAdapterRemoteException In case communication with remote system fails. * @throws InvalidOperationContextException In case operation context is invalid. */ - @RequestMapping(value = "/action", method = RequestMethod.POST) + @RequestMapping(value = "/execute", method = RequestMethod.POST) public ObjectResponse executeAfsAction(@RequestBody ObjectRequest request) throws DataAdapterRemoteException, InvalidOperationContextException { logger.info("Received executeAfsAction request for user: {}, operation ID: {}", request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); From d58ef2fd474267f62206c2a5a8e79b0a7644de2a Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Fri, 1 Nov 2019 11:59:55 -0400 Subject: [PATCH 69/79] Update sample AFS implementation to always return a label for AFS actions LOGIN_* and APPROVAL_* --- .../app/dataadapter/impl/service/DataAdapterService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index 66e10554..6373cd4d 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -447,6 +447,7 @@ public AfsResponse executeAfsAction(String userId, String organizationId, Operat AfsResponse response = new AfsResponse(); switch (afsRequestParameters.getAfsAction()) { case LOGIN_INIT: + case LOGIN_AUTH: case APPROVAL_AUTH: // Return AFS label, but do not apply response parameters on authentication form response.setAfsResponseApplied(false); @@ -470,7 +471,6 @@ public AfsResponse executeAfsAction(String userId, String organizationId, Operat } break; - case LOGIN_AUTH: case LOGOUT: // Do not apply response parameters response.setAfsResponseApplied(false); From 1033d9a981339cd5c58e25975eea6ce46c6d3d97 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Sat, 2 Nov 2019 10:11:34 -0400 Subject: [PATCH 70/79] Enable Spring actuator --- powerauth-data-adapter/pom.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/powerauth-data-adapter/pom.xml b/powerauth-data-adapter/pom.xml index bee456c3..940aea2d 100644 --- a/powerauth-data-adapter/pom.xml +++ b/powerauth-data-adapter/pom.xml @@ -77,6 +77,10 @@ org.springframework.boot spring-boot-starter-data-jpa + + org.springframework.boot + spring-boot-starter-actuator + From d2844a7a249ca8ee878047171119ac1adf2c1f6b Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Sat, 2 Nov 2019 10:41:44 -0400 Subject: [PATCH 71/79] Configuration properties cleanup --- .../src/main/resources/application.properties | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/powerauth-data-adapter/src/main/resources/application.properties b/powerauth-data-adapter/src/main/resources/application.properties index 9dac2721..70dbdc9f 100644 --- a/powerauth-data-adapter/src/main/resources/application.properties +++ b/powerauth-data-adapter/src/main/resources/application.properties @@ -1,16 +1,12 @@ # Allow externalization of properties using application-ext.properties spring.profiles.active=ext -# Database Keep-Alive -spring.datasource.test-while-idle=true -spring.datasource.test-on-borrow=true -spring.datasource.validation-query=SELECT 1 - # Database Configuration - MySQL spring.datasource.url=jdbc:mysql://localhost:3306/powerauth spring.datasource.username=powerauth spring.datasource.password= spring.datasource.driver-class-name=com.mysql.jdbc.Driver +spring.jpa.properties.hibernate.connection.CharSet=utf8mb4 spring.jpa.properties.hibernate.connection.characterEncoding=utf8 spring.jpa.properties.hibernate.connection.useUnicode=true From adcb5bcc703e2b07ccadd6d6e1f8bc644cbe4db8 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Tue, 12 Nov 2019 18:09:09 +0100 Subject: [PATCH 72/79] Add Travis build configuration with Coverity scan --- .travis.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..5ee37f2d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,23 @@ +language: java +jdk: + - openjdk11 +branches: + only: + - master + - coverity_scan +env: + global: + - secure: "ZeE+S5KU4rA0zta+udAFBIEETAqTg5SExaFtoNur/2DG79hcGGO/nC1VouwbwYnR81xNc3A5LniTsqwYjzFFstCRUPR7zjeYIq4B9v5bdNqdLiRvY/Re9Rk9f4hVf3039SnKGaEC+iCMzs/BxFW5+y68Oe1RKGh9yI0I2WKglkIw3NHLx+qI4HBhWwgso2USBNGGfEPOwJAR0940DxLywOxNFPUuMjYIFXY7Yx6Hko0L7BX+TRERmJuxK+cwoTWkFdmAzNIsau2WpSHN2/V+kIP8sr9eeUc33pY9ztW+oir3kteDhNjPeg9NA+FG6bGC8D8hPzNIMEED/OPs/Zj3G0oOLLTS/fjulSAiTAK6afkEyffNjVfh7dJQxvfGhHBJs1PDD0+a7sYogmf/eisxO6qYI+hRZMpkjjJL590Ovk6do9anzzmJ8lSSH6r8NVCANt2q5fnjp5BOOPFWGolWMvH2r699TiL3azBM1lWz3spvvk11DbtWqDJCOFmtD4SluTFAKuZjageQY69nPpB/w3Q+ThpYJqTNNrHD1i/b5daAi8aIGZud5vvwTUC5ItGzSGhjizTFxps7FOa77P//YIQhdirURvgIkuCf0A5vGBABuyeVOmz3pkx36sO4va1ZqNNYY9tbv5rdCBynU0KgmrOr6J3Q6IwMSxrc85ykSlI=" + +before_install: + - echo -n | openssl s_client -connect https://scan.coverity.com:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | sudo tee -a /etc/ssl/certs/ca- + +addons: + coverity_scan: + project: + name: "wultra/powerauth-webflow-customization" + description: "Build submitted via Travis CI" + notification_email: roman.strobl@wultra.com + build_command_prepend: "mvn -f powerauth-data-adapter/pom.xml clean" + build_command: "mvn -DskipTests=true -f powerauth-data-adapter/pom.xml compile" + branch_pattern: coverity_scan From b1b63494205663be2b3bc1fe9f03ed7c003a8d4f Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Tue, 12 Nov 2019 18:15:20 +0100 Subject: [PATCH 73/79] Support for blocked accounts in SMS verification --- .../app/dataadapter/impl/service/DataAdapterService.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index 6373cd4d..03402cbc 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -246,6 +246,15 @@ public VerifySmsAuthorizationResponse verifyAuthorizationSms(String userId, Stri public VerifySmsAndPasswordResponse verifyAuthorizationSmsAndPassword(String userId, String organizationId, AccountStatus accountStatus, String messageId, String authorizationCode, OperationContext operationContext, AuthenticationContext authenticationContext, String password) throws DataAdapterRemoteException, InvalidOperationContextException { VerifySmsAndPasswordResponse response = new VerifySmsAndPasswordResponse(); + // Skip credentials verification for non-existent user accounts or blocked user accounts, such request would always fail. + // Furthermore, do not leak information that account does not exist or it is blocked by providing a regular authentication error. + if (userId == null || accountStatus != AccountStatus.ACTIVE) { + response.setSmsAuthorizationResult(SmsAuthorizationResult.SKIPPED); + response.setUserAuthenticationResult(UserAuthenticationResult.SKIPPED); + response.setErrorMessage("login.authenticationFailed"); + return response; + } + // Verify authorization code from SMS VerifySmsAuthorizationResponse smsResponse = smsPersistenceService.verifyAuthorizationSms(messageId, authorizationCode, true); authenticationContext.setSmsAuthorizationResult(smsResponse.getSmsAuthorizationResult()); From 32aee7fd95938a9df00e3de04b91bb26a681c5cb Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Tue, 12 Nov 2019 18:18:38 +0100 Subject: [PATCH 74/79] Support for blocked accounts in SMS verification --- .../impl/service/DataAdapterService.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index 03402cbc..0b4d7eb7 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -234,7 +234,18 @@ public CreateSmsAuthorizationResponse createAndSendAuthorizationSms(String userI @Override public VerifySmsAuthorizationResponse verifyAuthorizationSms(String userId, String organizationId, AccountStatus accountStatus, String messageId, String authorizationCode, OperationContext operationContext) throws DataAdapterRemoteException, InvalidOperationContextException { // You can override this logic in case more complex handling of SMS verification is required. - VerifySmsAuthorizationResponse response = smsPersistenceService.verifyAuthorizationSms(messageId, authorizationCode, false); + VerifySmsAuthorizationResponse response; + + // Skip credentials verification for non-existent user accounts or blocked user accounts, such request would always fail. + // Furthermore, do not leak information that account does not exist or it is blocked by providing a regular authentication error. + if (userId == null || accountStatus != AccountStatus.ACTIVE) { + response = new VerifySmsAuthorizationResponse(); + response.setSmsAuthorizationResult(SmsAuthorizationResult.SKIPPED); + response.setErrorMessage("login.authenticationFailed"); + return response; + } + + response = smsPersistenceService.verifyAuthorizationSms(messageId, authorizationCode, false); // Set number of remaining attempts for verification in case it is available. // authResponse.setRemainingAttempts(5); // You can enable showing of remaining attempts for the operation. From a0875d405feadceed810ad969bf018f1d4ec04b6 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Tue, 12 Nov 2019 19:00:18 +0100 Subject: [PATCH 75/79] Add custom Maven compilation step --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 5ee37f2d..42e30564 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: java jdk: - openjdk11 +script: mvn -f powerauth-data-adapter/pom.xml clean package branches: only: - master From f8e5dd00850ff4294c015de55f40f265c267620a Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Tue, 12 Nov 2019 20:01:58 +0100 Subject: [PATCH 76/79] Update Spring Boot due to security advisories --- powerauth-data-adapter/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powerauth-data-adapter/pom.xml b/powerauth-data-adapter/pom.xml index 940aea2d..d7795369 100644 --- a/powerauth-data-adapter/pom.xml +++ b/powerauth-data-adapter/pom.xml @@ -14,7 +14,7 @@ org.springframework.boot spring-boot-starter-parent - 2.1.9.RELEASE + 2.2.1.RELEASE From 5512783f8e36c8d6eba88ac568480b8ae8cb28e8 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Thu, 19 Dec 2019 22:29:31 +0100 Subject: [PATCH 77/79] Update powerauth-crypto version --- powerauth-data-adapter/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powerauth-data-adapter/pom.xml b/powerauth-data-adapter/pom.xml index d7795369..e22a3dce 100644 --- a/powerauth-data-adapter/pom.xml +++ b/powerauth-data-adapter/pom.xml @@ -98,7 +98,7 @@ io.getlime.security powerauth-java-crypto - 0.23.0-SNAPSHOT + 0.23.0 From 8855b04754c93cab7712e8cda221ae59e6cf65e8 Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Mon, 20 Jan 2020 15:22:57 +0100 Subject: [PATCH 78/79] Fix issues found during code review --- README.md | 2 +- docs/Customizing-Web-Flow-Appearance.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 95b67918..82c85842 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ and other changes required for customizing Web Flow for clients. ## Documentation -For the most recent documentation and tutorials, please visit [PowerAuth Web Flow Customization Documentation](https://developers.wultra.com/docs/current/powerauth-webflow-customization/). +For the most recent documentation and tutorials, please visit [PowerAuth Web Flow Customization Documentation](https://developers.wultra.com/docs/develop/powerauth-webflow-customization/). ## License diff --git a/docs/Customizing-Web-Flow-Appearance.md b/docs/Customizing-Web-Flow-Appearance.md index 00b66fca..e70ee4f6 100644 --- a/docs/Customizing-Web-Flow-Appearance.md +++ b/docs/Customizing-Web-Flow-Appearance.md @@ -66,7 +66,7 @@ The OAuth 2.0 consent form used by Web Flow can be customized by implementing fo ### Initialize Consent Form The [initConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L177) method is used to -allow to decide whether consent form for should be displayed for given operation context. Based on values of parameters `userId`, `organizationId` +allow to decide whether consent form should be displayed for given operation context. Based on values of parameters `userId`, `organizationId` and `operationContext` a decision can be made whether to display the consent form or not. In case the consent form is always displayed, return true in response unconditionally. From 909811a9d407e59d1a5b042c2ed91bb94a401ccb Mon Sep 17 00:00:00 2001 From: Roman Strobl Date: Mon, 20 Jan 2020 16:22:16 +0100 Subject: [PATCH 79/79] Update version for release 0.23.0 --- powerauth-data-adapter/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/powerauth-data-adapter/pom.xml b/powerauth-data-adapter/pom.xml index e22a3dce..825ef93a 100644 --- a/powerauth-data-adapter/pom.xml +++ b/powerauth-data-adapter/pom.xml @@ -5,7 +5,7 @@ powerauth-data-adapter io.getlime.security - 0.23.0-SNAPSHOT + 0.23.0 war powerauth-data-adapter @@ -93,7 +93,7 @@ io.getlime.security powerauth-data-adapter-model - 0.23.0-SNAPSHOT + 0.23.0 io.getlime.security