diff --git a/.github/workflows/develop_pa-test-internal-testserver-app.yml b/.github/workflows/develop_pa-test-internal-testserver-app.yml index 427b5c25..19de3f95 100644 --- a/.github/workflows/develop_pa-test-internal-testserver-app.yml +++ b/.github/workflows/develop_pa-test-internal-testserver-app.yml @@ -1,30 +1,36 @@ # Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy # More GitHub Actions for Azure: https://github.com/Azure/actions -name: Build and deploy container app to Azure Web App - pa-test-internal-testserver-app +name: Build and deploy container app to Container Registries on: push: branches: - develop workflow_dispatch: - + inputs: + jfrog_deploy: + type: boolean + description: Check if build image should be uploaded to JFrog + default: false + required: false jobs: build: runs-on: 'ubuntu-latest' environment: test steps: - - uses: actions/checkout@v3 - - uses: actions/setup-java@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 with: - java-version: '17' + java-version: '21' distribution: 'temurin' server-id: jfrog-central server-username: INTERNAL_USERNAME server-password: INTERNAL_PASSWORD cache: maven - - name: Set Timestamp for docker image tag + - name: Set Timestamp for docker image for development branch + if: github.ref == 'refs/heads/develop' run: echo "TIMESTAMP=-$(date +%Y.%m.%d)" >> $GITHUB_ENV - name: Get Powerauth Test Server version run: | @@ -42,9 +48,9 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - - name: Log in to registry + - name: Log in to ACR if: ${{ github.actor != 'dependabot[bot]' }} - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: https://powerauthextendedtest.azurecr.io/ username: ${{ secrets.AZUREAPPSERVICE_CONTAINERUSERNAME }} @@ -53,12 +59,32 @@ jobs: run: | cd powerauth-test-server ./copy_liquibase.sh - - name: Build and push container image to registry - uses: docker/build-push-action@v2 + - name: Build and push container image to ACR + uses: docker/build-push-action@v5 with: push: ${{ github.actor != 'dependabot[bot]' }} + platforms: linux/amd64,linux/arm64 tags: powerauthextendedtest.azurecr.io/powerauth-test-server:${{ env.REVISION }}${{ env.TIMESTAMP }}-${{ github.sha }} file: ./powerauth-test-server/Dockerfile context: ./powerauth-test-server/ - + cache-from: type=gha + cache-to: type=gha,mode=max + - name: Log in to JFrog + if: ${{ github.event_name == 'workflow_dispatch' && inputs.jfrog_deploy == true }} + uses: docker/login-action@v3 + with: + registry: https://wultra.jfrog.io/ + username: ${{ secrets.JFROG_CONTAINER_REGISTRY_USERNAME }} + password: ${{ secrets.JFROG_CONTAINER_REGISTRY_PASSWORD }} + - name: Build and push container image to JFrog + if: ${{ github.event_name == 'workflow_dispatch' && inputs.jfrog_deploy == true }} + uses: docker/build-push-action@v5 + with: + push: ${{ github.event_name == 'workflow_dispatch' && inputs.jfrog_deploy == true }} + platforms: linux/amd64,linux/arm64 + tags: wultra.jfrog.io/wultra-docker/powerauth-test-server:${{ env.REVISION }}${{ env.TIMESTAMP }} + file: ./powerauth-test-server/Dockerfile + context: ./powerauth-test-server/ + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/maven-deploy.yml b/.github/workflows/maven-deploy.yml new file mode 100644 index 00000000..f098c09d --- /dev/null +++ b/.github/workflows/maven-deploy.yml @@ -0,0 +1,22 @@ +name: Deploy with Maven + +on: + workflow_dispatch: + branches: + - 'develop' + - 'master' + - 'releases/*' + +jobs: + maven-deploy-manual: + if: ${{ github.event_name == 'workflow_dispatch' }} + name: Manual deploy + uses: wultra/wultra-infrastructure/.github/workflows/maven-deploy.yml@develop + with: + environment: internal-publish + release_type: snapshot + directory_path: ./powerauth-test-server + java_version: 21 + secrets: + username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} diff --git a/.github/workflows/maven-integration-test.yml b/.github/workflows/maven-integration-test.yml index cdbefeac..78fb9c0f 100644 --- a/.github/workflows/maven-integration-test.yml +++ b/.github/workflows/maven-integration-test.yml @@ -21,11 +21,11 @@ jobs: runs-on: ubuntu-latest environment: ${{ inputs.environment || 'dev' }} steps: - - uses: actions/checkout@v3 - - name: Set up JDK 17 - uses: actions/setup-java@v3 + - uses: actions/checkout@v4 + - name: Set up JDK 21 + uses: actions/setup-java@v4 with: - java-version: 17 + java-version: 21 distribution: 'temurin' server-id: jfrog-central server-username: INTERNAL_USERNAME @@ -46,7 +46,7 @@ jobs: POWERAUTH_SERVICE_SECURITY_CLIENTSECRET: ${{ secrets.POWERAUTH_SERVICE_SECURITY_CLIENTSECRET }} POWERAUTH_TEST_INCLUDECUSTOMTESTS: ${{ inputs.includeCustomTests == '' || inputs.includeCustomTests }} # default includeCustomTests=true even for 'schedule' event - name: Publish Test Report - uses: mikepenz/action-junit-report@v3 + uses: mikepenz/action-junit-report@v4 if: always() with: detailed_summary: true diff --git a/.github/workflows/maven-test-compile.yml b/.github/workflows/maven-test-compile.yml index ceeaeb0a..313c7283 100644 --- a/.github/workflows/maven-test-compile.yml +++ b/.github/workflows/maven-test-compile.yml @@ -18,11 +18,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up JDK ${{ inputs.java_version }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: - java-version: '17' + java-version: '21' distribution: 'temurin' server-id: jfrog-central server-username: INTERNAL_USERNAME diff --git a/.run/PowerauthFido2TestApplication.run.xml b/.run/PowerauthFido2TestApplication.run.xml new file mode 100644 index 00000000..3bd0c80f --- /dev/null +++ b/.run/PowerauthFido2TestApplication.run.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/docs-private/Developer-How-To-Start.md b/docs-private/Developer-How-To-Start.md index 5c5376d2..6a476e01 100644 --- a/docs-private/Developer-How-To-Start.md +++ b/docs-private/Developer-How-To-Start.md @@ -18,3 +18,12 @@ Others (like URL, username, password) depend on your environment. ```shell liquibase --changelog-file=./docs/db/changelog/changesets/powerauth-test-server/db.changelog-module.xml --url=jdbc:postgresql://localhost:5432/powerauth --username=powerauth status ``` + +## PowerAuth FIDO2 Tests + +### Standalone Run + +- Enable maven profile `standalone` +- Use IntelliJ Idea run configuration at `../.run/PowerAuthFido2TestApplication.run.xml` +- Open [http://localhost:8083/powerauth-fido2-test/actuator/health](http://localhost:8083/powerauth-fido2-test/actuator/health) and you should get `{"status":"UP"}` + diff --git a/lombok.config b/lombok.config new file mode 100644 index 00000000..1bb49ca0 --- /dev/null +++ b/lombok.config @@ -0,0 +1 @@ +lombok.log.fieldName=logger \ No newline at end of file diff --git a/pom.xml b/pom.xml index 7fbb8242..eeaa197f 100644 --- a/pom.xml +++ b/pom.xml @@ -8,13 +8,13 @@ org.springframework.boot spring-boot-starter-parent - 3.1.6 + 3.2.4 com.wultra powerauth-backend-tests-parent - 1.6.0 + 1.7.0 pom Parent pom for backend tests @@ -45,22 +45,25 @@ - 1.6.0 - 1.6.0 - 1.6.0 - 1.6.0 - 1.6.0 - 1.8.0 + 1.7.0 + 1.7.0 + 1.7.0 + 1.7.0 + 1.7.0 + 1.7.0 + 1.9.0 1.77 - 2.3.0 + 2.5.0 7.4 - - 1.4.14 + 0.23.0.RELEASE + + true powerauth-backend-tests + powerauth-fido2-tests powerauth-load-tests powerauth-test-server powerauth-webflow-tests @@ -183,6 +186,18 @@ true + + + jfrog-central + Wultra Artifactory-releases + https://wultra.jfrog.io/artifactory/internal-maven-repository + + + jfrog-central + Wultra Artifactory-snapshots + https://wultra.jfrog.io/artifactory/internal-maven-repository + + jfrog-central @@ -218,9 +233,6 @@ !useInternalRepo - - true - ossrh-snapshots diff --git a/powerauth-backend-tests/README.md b/powerauth-backend-tests/README.md index b6101ad7..0ec4490a 100644 --- a/powerauth-backend-tests/README.md +++ b/powerauth-backend-tests/README.md @@ -60,7 +60,6 @@ File `powerauth-java.server.xml`: - ``` @@ -71,7 +70,6 @@ File `enrollment-server.xml`: - diff --git a/powerauth-backend-tests/pom.xml b/powerauth-backend-tests/pom.xml index 94ef6826..cd78b524 100644 --- a/powerauth-backend-tests/pom.xml +++ b/powerauth-backend-tests/pom.xml @@ -8,7 +8,7 @@ com.wultra powerauth-backend-tests-parent - 1.6.0 + 1.7.0 com.wultra diff --git a/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/shared/PowerAuthApiShared.java b/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/shared/PowerAuthApiShared.java index 50006570..4d5d5b1c 100644 --- a/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/shared/PowerAuthApiShared.java +++ b/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/shared/PowerAuthApiShared.java @@ -19,11 +19,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.wultra.security.powerauth.client.PowerAuthClient; -import com.wultra.security.powerauth.client.model.entity.*; -import com.wultra.security.powerauth.client.model.enumeration.*; +import com.wultra.security.powerauth.client.model.entity.SignatureAuditItem; +import com.wultra.security.powerauth.client.model.enumeration.ActivationStatus; +import com.wultra.security.powerauth.client.model.enumeration.SignatureType; import com.wultra.security.powerauth.client.model.error.PowerAuthClientException; -import com.wultra.security.powerauth.client.model.request.GetActivationListForUserRequest; -import com.wultra.security.powerauth.client.model.request.GetEciesDecryptorRequest; import com.wultra.security.powerauth.client.model.response.*; import com.wultra.security.powerauth.configuration.PowerAuthTestConfiguration; import io.getlime.security.powerauth.crypto.client.activation.PowerAuthClientActivation; @@ -34,15 +33,12 @@ import io.getlime.security.powerauth.crypto.lib.config.SignatureConfiguration; import io.getlime.security.powerauth.crypto.lib.encryptor.ClientEncryptor; import io.getlime.security.powerauth.crypto.lib.encryptor.EncryptorFactory; -import io.getlime.security.powerauth.crypto.lib.encryptor.ServerEncryptor; -import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.EciesEnvelopeKey; import io.getlime.security.powerauth.crypto.lib.encryptor.exception.EncryptorException; import io.getlime.security.powerauth.crypto.lib.encryptor.model.EncryptedRequest; import io.getlime.security.powerauth.crypto.lib.encryptor.model.EncryptedResponse; import io.getlime.security.powerauth.crypto.lib.encryptor.model.EncryptorId; import io.getlime.security.powerauth.crypto.lib.encryptor.model.EncryptorParameters; import io.getlime.security.powerauth.crypto.lib.encryptor.model.v3.ClientEncryptorSecrets; -import io.getlime.security.powerauth.crypto.lib.encryptor.model.v3.ServerEncryptorSecrets; import io.getlime.security.powerauth.crypto.lib.enums.PowerAuthSignatureTypes; import io.getlime.security.powerauth.crypto.lib.generator.KeyGenerator; import io.getlime.security.powerauth.crypto.lib.model.exception.CryptoProviderException; @@ -74,8 +70,10 @@ import java.security.PrivateKey; import java.security.PublicKey; import java.security.spec.InvalidKeySpecException; -import java.time.Duration; -import java.util.*; +import java.util.Base64; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.List; import static org.junit.jupiter.api.Assertions.*; @@ -99,190 +97,6 @@ public class PowerAuthApiShared { private static final int TIME_SYNCHRONIZATION_WINDOW_SECONDS = 60; - public static void systemStatusTest(PowerAuthClient powerAuthClient) throws PowerAuthClientException { - final GetSystemStatusResponse response = powerAuthClient.getSystemStatus(); - assertEquals("OK", response.getStatus()); - } - - public static void errorListTest(PowerAuthClient powerAuthClient) throws PowerAuthClientException { - final GetErrorCodeListResponse response = powerAuthClient.getErrorList(Locale.ENGLISH.getLanguage()); - assertTrue(response.getErrors().size() > 32); - } - - public static void initActivationTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config, String version) throws PowerAuthClientException { - final InitActivationResponse response = powerAuthClient.initActivation(config.getUser(version), config.getApplicationId()); - assertNotNull(response.getActivationId()); - assertNotNull(response.getActivationCode()); - assertNotNull(response.getActivationSignature()); - assertEquals(config.getUser(version), response.getUserId()); - assertEquals(config.getApplicationId(), response.getApplicationId()); - final GetActivationStatusResponse statusResponse = powerAuthClient.getActivationStatus(response.getActivationId()); - assertEquals(ActivationStatus.CREATED, statusResponse.getActivationStatus()); - } - - public static void prepareActivationTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config, String version) throws CryptoProviderException, EncryptorException, IOException, PowerAuthClientException { - String activationName = "test_prepare"; - InitActivationResponse response = powerAuthClient.initActivation(config.getUser(version), config.getApplicationId()); - String activationId = response.getActivationId(); - String activationCode = response.getActivationCode(); - KeyPair deviceKeyPair = CLIENT_ACTIVATION.generateDeviceKeyPair(); - byte[] devicePublicKeyBytes = KEY_CONVERTOR.convertPublicKeyToBytes(deviceKeyPair.getPublic()); - String devicePublicKeyBase64 = Base64.getEncoder().encodeToString(devicePublicKeyBytes); - ActivationLayer2Request requestL2 = new ActivationLayer2Request(); - requestL2.setActivationName(activationName); - requestL2.setDevicePublicKey(devicePublicKeyBase64); - ClientEncryptor clientEncryptorL2 = ENCRYPTOR_FACTORY.getClientEncryptor( - EncryptorId.ACTIVATION_LAYER_2, - new EncryptorParameters(version, config.getApplicationKey(), null), - new ClientEncryptorSecrets(config.getMasterPublicKey(), config.getApplicationSecret()) - ); - ByteArrayOutputStream baosL2 = new ByteArrayOutputStream(); - OBJECT_MAPPER.writeValue(baosL2, requestL2); - EncryptedRequest encryptedRequestL2 = clientEncryptorL2.encryptRequest(baosL2.toByteArray()); - PrepareActivationResponse prepareResponse = powerAuthClient.prepareActivation(activationCode, config.getApplicationKey(), true, encryptedRequestL2.getEphemeralPublicKey(), encryptedRequestL2.getEncryptedData(), encryptedRequestL2.getMac(), encryptedRequestL2.getNonce(), version, encryptedRequestL2.getTimestamp()); - assertEquals(ActivationStatus.PENDING_COMMIT, prepareResponse.getActivationStatus()); - CommitActivationResponse commitResponse = powerAuthClient.commitActivation(activationId, config.getUser(version)); - assertTrue(commitResponse.isActivated()); - } - - public static void createActivationTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config, String version) throws CryptoProviderException, EncryptorException, IOException, PowerAuthClientException { - String activationName = "test_create"; - KeyPair deviceKeyPair = CLIENT_ACTIVATION.generateDeviceKeyPair(); - byte[] devicePublicKeyBytes = KEY_CONVERTOR.convertPublicKeyToBytes(deviceKeyPair.getPublic()); - String devicePublicKeyBase64 = Base64.getEncoder().encodeToString(devicePublicKeyBytes); - ActivationLayer2Request requestL2 = new ActivationLayer2Request(); - requestL2.setActivationName(activationName); - requestL2.setDevicePublicKey(devicePublicKeyBase64); - ClientEncryptor clientEncryptorL2 = ENCRYPTOR_FACTORY.getClientEncryptor( - EncryptorId.ACTIVATION_LAYER_2, - new EncryptorParameters(version, config.getApplicationKey(), null), - new ClientEncryptorSecrets(config.getMasterPublicKey(), config.getApplicationSecret()) - ); - ByteArrayOutputStream baosL2 = new ByteArrayOutputStream(); - OBJECT_MAPPER.writeValue(baosL2, requestL2); - EncryptedRequest encryptedRequestL2 = clientEncryptorL2.encryptRequest(baosL2.toByteArray()); - CreateActivationResponse createResponse = powerAuthClient.createActivation(config.getUser(version), null, - null, config.getApplicationKey(), encryptedRequestL2.getEphemeralPublicKey(), encryptedRequestL2.getEncryptedData(), encryptedRequestL2.getMac(), encryptedRequestL2.getNonce(), version, encryptedRequestL2.getTimestamp()); - String activationId = createResponse.getActivationId(); - assertNotNull(activationId); - GetActivationStatusResponse statusResponse = powerAuthClient.getActivationStatus(activationId); - assertEquals(ActivationStatus.PENDING_COMMIT, statusResponse.getActivationStatus()); - CommitActivationResponse commitResponse = powerAuthClient.commitActivation(activationId, config.getUser(version)); - assertTrue(commitResponse.isActivated()); - } - - public static void updateActivationOtpAndCommitTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config, String version) throws CryptoProviderException, EncryptorException, IOException, PowerAuthClientException { - String activationName = "test_update_otp"; - InitActivationResponse response = powerAuthClient.initActivation(config.getUser(version), config.getApplicationId(), ActivationOtpValidation.NONE, null); - String activationId = response.getActivationId(); - String activationCode = response.getActivationCode(); - KeyPair deviceKeyPair = CLIENT_ACTIVATION.generateDeviceKeyPair(); - byte[] devicePublicKeyBytes = KEY_CONVERTOR.convertPublicKeyToBytes(deviceKeyPair.getPublic()); - String devicePublicKeyBase64 = Base64.getEncoder().encodeToString(devicePublicKeyBytes); - ActivationLayer2Request requestL2 = new ActivationLayer2Request(); - requestL2.setActivationName(activationName); - requestL2.setDevicePublicKey(devicePublicKeyBase64); - ClientEncryptor clientEncryptorL2 = ENCRYPTOR_FACTORY.getClientEncryptor( - EncryptorId.ACTIVATION_LAYER_2, - new EncryptorParameters(version, config.getApplicationKey(), null), - new ClientEncryptorSecrets(config.getMasterPublicKey(), config.getApplicationSecret()) - ); - ByteArrayOutputStream baosL2 = new ByteArrayOutputStream(); - OBJECT_MAPPER.writeValue(baosL2, requestL2); - EncryptedRequest encryptedRequestL2 = clientEncryptorL2.encryptRequest(baosL2.toByteArray()); - PrepareActivationResponse prepareResponse = powerAuthClient.prepareActivation(activationCode, config.getApplicationKey(), true, encryptedRequestL2.getEphemeralPublicKey(), encryptedRequestL2.getEncryptedData(), encryptedRequestL2.getMac(), encryptedRequestL2.getNonce(), version, encryptedRequestL2.getTimestamp()); - assertEquals(ActivationStatus.PENDING_COMMIT, prepareResponse.getActivationStatus()); - UpdateActivationOtpResponse otpResponse = powerAuthClient.updateActivationOtp(activationId, config.getUser(version), "12345678"); - assertTrue(otpResponse.isUpdated()); - CommitActivationResponse commitResponse = powerAuthClient.commitActivation(activationId, config.getUser(version), "12345678"); - assertTrue(commitResponse.isActivated()); - } - - public static void removeActivationTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config, String version) throws PowerAuthClientException { - InitActivationResponse response = powerAuthClient.initActivation(config.getUser(version), config.getApplicationId()); - GetActivationStatusResponse statusResponse = powerAuthClient.getActivationStatus(response.getActivationId()); - assertEquals(ActivationStatus.CREATED, statusResponse.getActivationStatus()); - RemoveActivationResponse removeResponse = powerAuthClient.removeActivation(response.getActivationId(), null); - assertTrue(removeResponse.isRemoved()); - GetActivationStatusResponse statusResponse2 = powerAuthClient.getActivationStatus(response.getActivationId()); - assertEquals(ActivationStatus.REMOVED, statusResponse2.getActivationStatus()); - } - - public static void activationListForUserTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config, String version) throws PowerAuthClientException { - InitActivationResponse response = powerAuthClient.initActivation(config.getUser(version), config.getApplicationId()); - GetActivationStatusResponse statusResponse = powerAuthClient.getActivationStatus(response.getActivationId()); - assertEquals(ActivationStatus.CREATED, statusResponse.getActivationStatus()); - final List listResponse = powerAuthClient.getActivationListForUser(config.getUser(version)); - assertNotEquals(0, listResponse.size()); - } - - public static void testGetActivationListForUserPagination(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config, String version) throws PowerAuthClientException { - // Prepare the base GetActivationListForUserRequest - final GetActivationListForUserRequest baseRequest = new GetActivationListForUserRequest(); - baseRequest.setUserId(config.getUser(version)); - baseRequest.setApplicationId(config.getApplicationId()); - - // Create a list to store the activation IDs - final List activationIds = new ArrayList<>(); - - // Create multiple activations for the test user - for (int i = 0; i < 10; i++) { - InitActivationResponse initResponse = powerAuthClient.initActivation(baseRequest.getUserId(), baseRequest.getApplicationId()); - activationIds.add(initResponse.getActivationId()); - } - - // Prepare the request for the first page of activations - final GetActivationListForUserRequest requestPage1 = new GetActivationListForUserRequest(); - requestPage1.setUserId(baseRequest.getUserId()); - requestPage1.setApplicationId(baseRequest.getApplicationId()); - requestPage1.setPageNumber(0); - requestPage1.setPageSize(5); - - // Fetch the first page of activations - final GetActivationListForUserResponse responsePage1 = powerAuthClient.getActivationListForUser(requestPage1); - assertEquals(5, responsePage1.getActivations().size()); - - // Prepare the request for the second page of activations - final GetActivationListForUserRequest requestPage2 = new GetActivationListForUserRequest(); - requestPage2.setUserId(baseRequest.getUserId()); - requestPage2.setApplicationId(baseRequest.getApplicationId()); - requestPage2.setPageNumber(1); - requestPage2.setPageSize(5); - - // Fetch the second page of activations - final GetActivationListForUserResponse responsePage2 = powerAuthClient.getActivationListForUser(requestPage2); - assertEquals(5, responsePage2.getActivations().size()); - - // Check that the activations on the different pages are not the same - assertNotEquals(responsePage1.getActivations(), responsePage2.getActivations()); - - // Clean up the created activations at the end - for (String id : activationIds) { - RemoveActivationResponse removeActivationResponse = powerAuthClient.removeActivation(id, config.getUser(version)); - assertTrue(removeActivationResponse.isRemoved()); - } - } - - public static void lookupActivationsTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config, String version) throws PowerAuthClientException { - InitActivationResponse response = powerAuthClient.initActivation(config.getUser(version), config.getApplicationId()); - GetActivationStatusResponse statusResponse = powerAuthClient.getActivationStatus(response.getActivationId()); - final Date timestampCreated = statusResponse.getTimestampCreated(); - assertEquals(ActivationStatus.CREATED, statusResponse.getActivationStatus()); - List activations = powerAuthClient.lookupActivations(Collections.singletonList(config.getUser(version)), Collections.singletonList(config.getApplicationId()), - null, timestampCreated, ActivationStatus.CREATED, null); - assertTrue(activations.size() >= 1); - } - - public static void activationStatusUpdateTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config, String version) throws PowerAuthClientException { - InitActivationResponse response = powerAuthClient.initActivation(config.getUser(version), config.getApplicationId()); - GetActivationStatusResponse statusResponse = powerAuthClient.getActivationStatus(response.getActivationId()); - assertEquals(ActivationStatus.CREATED, statusResponse.getActivationStatus()); - UpdateStatusForActivationsResponse updateResponse = powerAuthClient.updateStatusForActivations(Collections.singletonList(response.getActivationId()), ActivationStatus.REMOVED); - assertTrue(updateResponse.isUpdated()); - GetActivationStatusResponse statusResponse2 = powerAuthClient.getActivationStatus(response.getActivationId()); - assertEquals(ActivationStatus.REMOVED, statusResponse2.getActivationStatus()); - } - public static void verifySignatureTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config, String version) throws GenericCryptoException, CryptoProviderException, InvalidKeyException, PowerAuthClientException { Calendar before = new GregorianCalendar(); before.add(Calendar.SECOND, -TIME_SYNCHRONIZATION_WINDOW_SECONDS); @@ -307,7 +121,7 @@ public static void verifySignatureTest(PowerAuthClient powerAuthClient, PowerAut after.add(Calendar.SECOND, TIME_SYNCHRONIZATION_WINDOW_SECONDS); List auditItems = powerAuthClient.getSignatureAuditLog(config.getUser(version), config.getApplicationId(), before.getTime(), after.getTime()); boolean signatureFound = false; - for (SignatureAuditItem item: auditItems) { + for (SignatureAuditItem item : auditItems) { if (signatureValue.equals(item.getSignature())) { assertEquals(config.getActivationId(version), item.getActivationId()); assertEquals(normalizedDataWithSecret, new String(Base64.getDecoder().decode(item.getDataBase64()))); @@ -323,26 +137,6 @@ public static void verifySignatureTest(PowerAuthClient powerAuthClient, PowerAut assertTrue(signatureFound); } - public static void nonPersonalizedOfflineSignaturePayloadTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config) throws PowerAuthClientException { - // For more complete tests for createNonPersonalizedOfflineSignaturePayload see PowerAuthSignatureTest - CreateNonPersonalizedOfflineSignaturePayloadResponse response = powerAuthClient.createNonPersonalizedOfflineSignaturePayload(config.getApplicationId(), "test_data"); - assertNotNull(response.getOfflineData()); - assertNotNull(response.getNonce()); - } - - public static void personalizedOfflineSignaturePayloadTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config, String version) throws PowerAuthClientException { - // For more complete tests for createPersonalizedOfflineSignaturePayload see PowerAuthSignatureTest - CreatePersonalizedOfflineSignaturePayloadResponse response = powerAuthClient.createPersonalizedOfflineSignaturePayload(config.getActivationId(version), "test_data"); - assertNotNull(response.getOfflineData()); - assertNotNull(response.getNonce()); - } - - public static void verifyOfflineSignatureTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config, String version) throws PowerAuthClientException { - // For more complete tests for verifyOfflineSignature see PowerAuthSignatureTest - VerifyOfflineSignatureResponse response = powerAuthClient.verifyOfflineSignature(config.getActivationId(version), "test_data", "12345678", false); - assertFalse(response.isSignatureValid()); - } - public static void unlockVaultAndECDSASignatureTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config, String version) throws GenericCryptoException, CryptoProviderException, InvalidKeySpecException, EncryptorException, IOException, InvalidKeyException, PowerAuthClientException { byte[] transportMasterKeyBytes = Base64.getDecoder().decode(JsonUtil.stringValue(config.getResultStatusObject(version), "transportMasterKey")); byte[] serverPublicKeyBytes = Base64.getDecoder().decode(JsonUtil.stringValue(config.getResultStatusObject(version), "serverPublicKey")); @@ -401,106 +195,8 @@ public static void unlockVaultAndECDSASignatureTest(PowerAuthClient powerAuthCli assertTrue(ecdsaResponse.isSignatureValid()); } - public static void activationHistoryTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config, String version) throws PowerAuthClientException { - InitActivationResponse response = powerAuthClient.initActivation(config.getUser(version) + "_history_test", config.getApplicationId()); - GetActivationStatusResponse statusResponse = powerAuthClient.getActivationStatus(response.getActivationId()); - assertEquals(ActivationStatus.CREATED, statusResponse.getActivationStatus()); - final Date before = statusResponse.getTimestampCreated(); - final Date after = Date.from(before.toInstant().plus(Duration.ofSeconds(1))); - final List activationHistory = powerAuthClient.getActivationHistory(response.getActivationId(), before, after); - final ActivationHistoryItem item = activationHistory.get(0); - assertEquals(response.getActivationId(), item.getActivationId()); - assertEquals(ActivationStatus.CREATED, item.getActivationStatus()); - } - - public static void blockAndUnblockActivationTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config, String version) throws PowerAuthClientException { - InitActivationResponse response = powerAuthClient.initActivation(config.getUser(version), config.getApplicationId()); - GetActivationStatusResponse statusResponse = powerAuthClient.getActivationStatus(response.getActivationId()); - assertEquals(ActivationStatus.CREATED, statusResponse.getActivationStatus()); - // Fake status change to ACTIVE for block and unblock test - UpdateStatusForActivationsResponse updateResponse = powerAuthClient.updateStatusForActivations(Collections.singletonList(response.getActivationId()), ActivationStatus.ACTIVE); - assertTrue(updateResponse.isUpdated()); - BlockActivationResponse blockResponse = powerAuthClient.blockActivation(response.getActivationId(), "TEST", null); - assertEquals(ActivationStatus.BLOCKED, blockResponse.getActivationStatus()); - GetActivationStatusResponse statusResponse2 = powerAuthClient.getActivationStatus(response.getActivationId()); - assertEquals(ActivationStatus.BLOCKED, statusResponse2.getActivationStatus()); - UnblockActivationResponse unblockResponse = powerAuthClient.unblockActivation(response.getActivationId(), null); - assertEquals(ActivationStatus.ACTIVE, unblockResponse.getActivationStatus()); - GetActivationStatusResponse statusResponse3 = powerAuthClient.getActivationStatus(response.getActivationId()); - assertEquals(ActivationStatus.ACTIVE, statusResponse3.getActivationStatus()); - } - - public static void applicationListTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config) throws PowerAuthClientException { - final GetApplicationListResponse applications = powerAuthClient.getApplicationList(); - assertNotEquals(0, applications.getApplications().size()); - boolean testApplicationFound = false; - for (Application app: applications.getApplications()) { - if (app.getApplicationId().equals(config.getApplicationId())) { - testApplicationFound = true; - } - } - assertTrue(testApplicationFound); - } - - public static void applicationDetailTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config) throws PowerAuthClientException { - GetApplicationDetailResponse response = powerAuthClient.getApplicationDetail(config.getApplicationId()); - assertEquals(config.getApplicationName(), response.getApplicationId()); - boolean testAppVersionFound = false; - for (ApplicationVersion version: response.getVersions()) { - if (version.getApplicationVersionId().equals(config.getApplicationVersionId())) { - testAppVersionFound = true; - } - } - assertTrue(testAppVersionFound); - } - - public static void applicationVersionLookupTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config) throws PowerAuthClientException { - LookupApplicationByAppKeyResponse response = powerAuthClient.lookupApplicationByAppKey(config.getApplicationKey()); - assertEquals(config.getApplicationId(), response.getApplicationId()); - } - // createApplication and createApplication version tests are skipped to avoid creating too many applications - public static void applicationSupportTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config) throws PowerAuthClientException { - UnsupportApplicationVersionResponse response = powerAuthClient.unsupportApplicationVersion(config.getApplicationId(), config.getApplicationVersionId()); - assertFalse(response.isSupported()); - SupportApplicationVersionResponse response2 = powerAuthClient.supportApplicationVersion(config.getApplicationId(), config.getApplicationVersionId()); - assertTrue(response2.isSupported()); - } - - public static void applicationIntegrationTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config) throws PowerAuthClientException { - String integrationName = UUID.randomUUID().toString(); - CreateIntegrationResponse response = powerAuthClient.createIntegration(integrationName); - assertEquals(integrationName, response.getName()); - final GetIntegrationListResponse items = powerAuthClient.getIntegrationList(); - boolean integrationFound = false; - for (Integration integration: items.getItems()) { - if (integration.getName().equals(integrationName)) { - integrationFound = true; - } - } - assertTrue(integrationFound); - RemoveIntegrationResponse removeResponse = powerAuthClient.removeIntegration(response.getId()); - assertTrue(removeResponse.isRemoved()); - } - - public static void callbackTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config) throws PowerAuthClientException { - String callbackName = UUID.randomUUID().toString(); - String url = "http://test.wultra.com/"; - CreateCallbackUrlResponse response = powerAuthClient.createCallbackUrl(config.getApplicationId(), callbackName, CallbackUrlType.ACTIVATION_STATUS_CHANGE, url, Collections.emptyList(), null); - assertEquals(callbackName, response.getName()); - final GetCallbackUrlListResponse items = powerAuthClient.getCallbackUrlList(config.getApplicationId()); - boolean callbackFound = false; - for (CallbackUrl callback: items.getCallbackUrlList()) { - if (callback.getName().equals(callbackName)) { - callbackFound = true; - } - } - assertTrue(callbackFound); - RemoveCallbackUrlResponse removeResponse = powerAuthClient.removeCallbackUrl(response.getId()); - assertTrue(removeResponse.isRemoved()); - } - public static void createValidateAndRemoveTokenTestActiveActivation(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config, String version) throws InvalidKeySpecException, CryptoProviderException, GenericCryptoException, IOException, EncryptorException, PowerAuthClientException { final TokenInfo tokenInfo = createToken(powerAuthClient, config, version); @@ -518,72 +214,6 @@ public static void createValidateAndRemoveTokenTestActiveActivation(PowerAuthCli assertTrue(removeResponse.isRemoved()); } - public static void createValidateAndRemoveTokenTestBlockedActivation(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config, String version) throws InvalidKeySpecException, CryptoProviderException, GenericCryptoException, IOException, EncryptorException, PowerAuthClientException { - final TokenInfo tokenInfo = createToken(powerAuthClient, config, version); - - // Block activation - final BlockActivationResponse blockResponse = powerAuthClient.blockActivation(config.getActivationId(version), "TEST", null); - assertEquals(ActivationStatus.BLOCKED, blockResponse.getActivationStatus()); - - // Check that token validation failed and activation status and blocked reason is available - final ValidateTokenResponse validateResponse = powerAuthClient.validateToken(tokenInfo.getTokenId(), - Base64.getEncoder().encodeToString(tokenInfo.getTokenNonce()), - version, - Long.parseLong(new String(tokenInfo.getTokenTimestamp())), - Base64.getEncoder().encodeToString(tokenInfo.getTokenDigest())); - assertFalse(validateResponse.isTokenValid()); - assertEquals(ActivationStatus.BLOCKED, validateResponse.getActivationStatus()); - assertEquals("TEST", validateResponse.getBlockedReason()); - - // Unblock activation - final UnblockActivationResponse unblockResponse = powerAuthClient.unblockActivation(config.getActivationId(version), "TEST"); - assertEquals(ActivationStatus.ACTIVE, unblockResponse.getActivationStatus()); - - final RemoveTokenResponse removeResponse = powerAuthClient.removeToken(tokenInfo.getTokenId(), config.getActivationId(version)); - assertTrue(removeResponse.isRemoved()); - } - - public static void getEciesDecryptorTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config, String version) throws EncryptorException, PowerAuthClientException { - String requestData = "test_data"; - ClientEncryptor clientEncryptor = ENCRYPTOR_FACTORY.getClientEncryptor( - EncryptorId.APPLICATION_SCOPE_GENERIC, - new EncryptorParameters(version, config.getApplicationKey(), null), - new ClientEncryptorSecrets(config.getMasterPublicKey(), config.getApplicationSecret()) - ); - EncryptedRequest encryptedRequest = clientEncryptor.encryptRequest(requestData.getBytes(StandardCharsets.UTF_8)); - final GetEciesDecryptorRequest eciesDecryptorRequest = new GetEciesDecryptorRequest(); - eciesDecryptorRequest.setProtocolVersion(version); - eciesDecryptorRequest.setActivationId(null); - eciesDecryptorRequest.setApplicationKey(config.getApplicationKey()); - eciesDecryptorRequest.setEphemeralPublicKey(encryptedRequest.getEphemeralPublicKey()); - eciesDecryptorRequest.setNonce(encryptedRequest.getNonce()); - eciesDecryptorRequest.setTimestamp(encryptedRequest.getTimestamp()); - GetEciesDecryptorResponse decryptorResponse = powerAuthClient.getEciesDecryptor(eciesDecryptorRequest); - - final byte[] secretKey = Base64.getDecoder().decode(decryptorResponse.getSecretKey()); - final byte[] sharedInfo2Base = Base64.getDecoder().decode(decryptorResponse.getSharedInfo2()); - final byte[] ephemeralPublicKeyBytes = Base64.getDecoder().decode(encryptedRequest.getEphemeralPublicKey()); - final EciesEnvelopeKey envelopeKey = new EciesEnvelopeKey(secretKey, ephemeralPublicKeyBytes); - final ServerEncryptor serverEncryptor = ENCRYPTOR_FACTORY.getServerEncryptor( - EncryptorId.APPLICATION_SCOPE_GENERIC, - new EncryptorParameters(version, config.getApplicationKey(), null), - new ServerEncryptorSecrets(secretKey, sharedInfo2Base) - ); - byte[] decryptedData = serverEncryptor.decryptRequest(encryptedRequest); - assertArrayEquals(requestData.getBytes(StandardCharsets.UTF_8), decryptedData); - } - - public static void recoveryCodeCreateLookupRevokeTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config, String version) throws PowerAuthClientException { - CreateRecoveryCodeResponse createResponse = powerAuthClient.createRecoveryCode(config.getApplicationId(), config.getUser(version), 2L); - assertEquals(config.getUser(version), createResponse.getUserId()); - assertEquals(RecoveryCodeStatus.CREATED, createResponse.getStatus()); - assertEquals(2, createResponse.getPuks().size()); - LookupRecoveryCodesResponse lookupResponse = powerAuthClient.lookupRecoveryCodes(config.getUser(version), null, config.getApplicationId(), RecoveryCodeStatus.CREATED, RecoveryPukStatus.VALID); - assertNotEquals(0, lookupResponse.getRecoveryCodes().size()); - RevokeRecoveryCodesResponse revokeResponse = powerAuthClient.revokeRecoveryCodes(Collections.singletonList(createResponse.getRecoveryCodeId())); - assertTrue(revokeResponse.isRevoked()); - } - public static void recoveryCodeConfirmAndActivationTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config, String version) throws CryptoProviderException, GenericCryptoException, IOException, EncryptorException, InvalidKeyException, InvalidKeySpecException, PowerAuthClientException { String activationName = "test_create_recovery"; KeyPair deviceKeyPair = CLIENT_ACTIVATION.generateDeviceKeyPair(); @@ -640,8 +270,8 @@ public static void recoveryCodeConfirmAndActivationTest(PowerAuthClient powerAut confirmResponse.getNonce(), confirmResponse.getTimestamp() )); - ConfirmRecoveryResponsePayload confirmResponsePayload = RestClientConfiguration.defaultMapper().readValue(confirmResponseRaw, ConfirmRecoveryResponsePayload.class); - assertTrue(confirmResponsePayload.getAlreadyConfirmed()); + final ConfirmRecoveryResponsePayload confirmResponsePayload = RestClientConfiguration.defaultMapper().readValue(confirmResponseRaw, ConfirmRecoveryResponsePayload.class); + assertTrue(confirmResponsePayload.isAlreadyConfirmed()); // Create recovery activation KeyPair deviceKeyPairR = CLIENT_ACTIVATION.generateDeviceKeyPair(); byte[] devicePublicKeyBytesR = KEY_CONVERTOR.convertPublicKeyToBytes(deviceKeyPairR.getPublic()); @@ -668,23 +298,6 @@ public static void recoveryCodeConfirmAndActivationTest(PowerAuthClient powerAut assertEquals(ActivationStatus.REMOVED, statusResponseR3.getActivationStatus()); } - public static void recoveryConfigTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config) throws PowerAuthClientException { - GetRecoveryConfigResponse response = powerAuthClient.getRecoveryConfig(config.getApplicationId()); - String remotePostcardPublicKey = response.getRemotePostcardPublicKey(); - assertNotNull(response.getPostcardPublicKey()); - assertNotNull(remotePostcardPublicKey); - UpdateRecoveryConfigResponse configResponse = powerAuthClient.updateRecoveryConfig(config.getApplicationId(), false, false, false, "test_key"); - assertTrue(configResponse.isUpdated()); - GetRecoveryConfigResponse response2 = powerAuthClient.getRecoveryConfig(config.getApplicationId()); - assertNotNull(response2.getPostcardPublicKey()); - assertFalse(response2.isActivationRecoveryEnabled()); - assertFalse(response2.isRecoveryPostcardEnabled()); - assertFalse(response2.isAllowMultipleRecoveryCodes()); - assertEquals("test_key", response2.getRemotePostcardPublicKey()); - UpdateRecoveryConfigResponse configResponse2 = powerAuthClient.updateRecoveryConfig(config.getApplicationId(), true, true, false, remotePostcardPublicKey); - assertTrue(configResponse2.isUpdated()); - } - // Activation flags are tested using PowerAuthActivationFlagsTest // Application roles are tested using PowerAuthApplicationRolesTest diff --git a/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/shared/PowerAuthApplicationRolesShared.java b/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/shared/PowerAuthApplicationRolesShared.java deleted file mode 100644 index 61223d57..00000000 --- a/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/shared/PowerAuthApplicationRolesShared.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * PowerAuth test and related software components - * Copyright (C) 2023 Wultra s.r.o. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package com.wultra.security.powerauth.test.shared; - -import com.wultra.security.powerauth.client.PowerAuthClient; -import com.wultra.security.powerauth.client.model.response.GetApplicationDetailResponse; -import com.wultra.security.powerauth.client.model.response.ListApplicationRolesResponse; -import com.wultra.security.powerauth.configuration.PowerAuthTestConfiguration; - -import java.util.Arrays; -import java.util.Collections; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -/** - * Application roles test shared logic. - * - * @author Roman Strobl, roman.strobl@wultra.com - */ -public class PowerAuthApplicationRolesShared { - - public static void applicationRolesCrudTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config) throws Exception { - // Test application roles CRUD - String applicationId = config.getApplicationId(); - - // Remove all existing roles - final ListApplicationRolesResponse initResponse = powerAuthClient.listApplicationRoles(applicationId); - if (!initResponse.getApplicationRoles().isEmpty()) { - powerAuthClient.removeApplicationRoles(applicationId, initResponse.getApplicationRoles()); - } - - powerAuthClient.addApplicationRoles(applicationId, Arrays.asList("ROLE1", "ROLE2")); - - final GetApplicationDetailResponse response = powerAuthClient.getApplicationDetail(applicationId); - assertEquals(Arrays.asList("ROLE1", "ROLE2"), response.getApplicationRoles()); - - ListApplicationRolesResponse listResponse = powerAuthClient.listApplicationRoles(applicationId); - assertEquals(Arrays.asList("ROLE1", "ROLE2"), listResponse.getApplicationRoles()); - - powerAuthClient.updateApplicationRoles(applicationId, Arrays.asList("ROLE3", "ROLE4")); - - ListApplicationRolesResponse listResponse2 = powerAuthClient.listApplicationRoles(applicationId); - assertEquals(Arrays.asList("ROLE3", "ROLE4"), listResponse2.getApplicationRoles()); - - powerAuthClient.removeApplicationRoles(applicationId, Collections.singletonList("ROLE4")); - - ListApplicationRolesResponse listResponse3 = powerAuthClient.listApplicationRoles(applicationId); - assertEquals(Collections.singletonList("ROLE3"), listResponse3.getApplicationRoles()); - - powerAuthClient.addApplicationRoles(applicationId, Arrays.asList("ROLE3", "ROLE4")); - - ListApplicationRolesResponse listResponse4 = powerAuthClient.listApplicationRoles(applicationId); - assertEquals(Arrays.asList("ROLE3", "ROLE4"), listResponse4.getApplicationRoles()); - } -} diff --git a/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/shared/PowerAuthCallbackShared.java b/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/shared/PowerAuthCallbackShared.java index 9107781f..38dcded9 100644 --- a/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/shared/PowerAuthCallbackShared.java +++ b/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/shared/PowerAuthCallbackShared.java @@ -39,64 +39,6 @@ */ public class PowerAuthCallbackShared { - public static void callbackCreateDeleteTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config) throws PowerAuthClientException { - String callbackName = UUID.randomUUID().toString(); - String callbackUrl = "http://test.test"; - powerAuthClient.createCallbackUrl(config.getApplicationId(), callbackName, CallbackUrlType.ACTIVATION_STATUS_CHANGE, callbackUrl, Collections.singletonList("activationId"), null); - final GetCallbackUrlListResponse callbacks = powerAuthClient.getCallbackUrlList(config.getApplicationId()); - boolean callbackFound = false; - for (CallbackUrl callback: callbacks.getCallbackUrlList()) { - if (callbackName.equals(callback.getName())) { - callbackFound = true; - assertEquals(callbackUrl, callback.getCallbackUrl()); - assertEquals(config.getApplicationId(), callback.getApplicationId()); - assertEquals(1, callback.getAttributes().size()); - assertEquals("activationId", callback.getAttributes().get(0)); - int callbackCountOrig = callbacks.getCallbackUrlList().size(); - powerAuthClient.removeCallbackUrl(callback.getId()); - } - } - assertTrue(callbackFound); - } - - public static void callbackUpdateTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config) throws PowerAuthClientException { - String callbackName = UUID.randomUUID().toString(); - String callbackUrl = "http://test.test"; - powerAuthClient.createCallbackUrl(config.getApplicationId(), callbackName, CallbackUrlType.ACTIVATION_STATUS_CHANGE, callbackUrl, Collections.singletonList("activationId"), null); - final GetCallbackUrlListResponse callbacks = powerAuthClient.getCallbackUrlList(config.getApplicationId()); - boolean callbackFound = false; - String callbackId = null; - for (CallbackUrl callback: callbacks.getCallbackUrlList()) { - if (callbackName.equals(callback.getName())) { - callbackFound = true; - callbackId = callback.getId(); - assertEquals(callbackUrl, callback.getCallbackUrl()); - assertEquals(config.getApplicationId(), callback.getApplicationId()); - assertEquals(1, callback.getAttributes().size()); - assertEquals("activationId", callback.getAttributes().get(0)); - } - } - assertTrue(callbackFound); - assertNotNull(callbackId); - String callbackName2 = UUID.randomUUID().toString(); - String callbackUrl2 = "http://test2.test2"; - powerAuthClient.updateCallbackUrl(callbackId, config.getApplicationId(), callbackName2, callbackUrl2, Arrays.asList("activationId", "userId", "deviceInfo", "platform"), null); - final GetCallbackUrlListResponse callbacks2 = powerAuthClient.getCallbackUrlList(config.getApplicationId()); - boolean callbackFound2 = false; - for (CallbackUrl callback: callbacks2.getCallbackUrlList()) { - if (callbackName2.equals(callback.getName())) { - callbackFound2 = true; - callbackId = callback.getId(); - assertEquals(callbackUrl2, callback.getCallbackUrl()); - assertEquals(config.getApplicationId(), callback.getApplicationId()); - assertEquals(4, callback.getAttributes().size()); - assertEquals(Arrays.asList("activationId", "userId", "deviceInfo", "platform"), callback.getAttributes()); - } - } - assertTrue(callbackFound2); - powerAuthClient.removeCallbackUrl(callbackId); - } - public static void callbackExecutionTest(PowerAuthClient powerAuthClient, PowerAuthTestConfiguration config, Integer port, String version) throws PowerAuthClientException, RestClientException { // Skip test when the tested PA server is not running on localhost assumeTrue(config.getPowerAuthRestUrl().contains("localhost:8080")); diff --git a/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/shared/PowerAuthIdentityVerificationShared.java b/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/shared/PowerAuthIdentityVerificationShared.java index 1bed1235..bbd2add5 100644 --- a/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/shared/PowerAuthIdentityVerificationShared.java +++ b/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/shared/PowerAuthIdentityVerificationShared.java @@ -21,7 +21,6 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; -import com.google.common.collect.ImmutableList; import com.wultra.app.enrollmentserver.api.model.onboarding.request.*; import com.wultra.app.enrollmentserver.api.model.onboarding.response.*; import com.wultra.app.enrollmentserver.model.enumeration.*; @@ -99,7 +98,7 @@ private static void processDocuments(final TestProcessContext processCtx, final approveConsent(ctx, processId); initIdentityVerification(ctx, activationId, processId); - final List idCardSubmits = ImmutableList.of( + final List idCardSubmits = List.of( FileSubmit.createFrom("images/id_card_mock_front.png", DocumentType.ID_CARD, CardSide.FRONT), FileSubmit.createFrom("images/id_card_mock_back.png", DocumentType.ID_CARD, CardSide.BACK) ); @@ -113,7 +112,7 @@ private static void processDocuments(final TestProcessContext processCtx, final assertIdentityVerificationStateWithRetries(ctx, new IdentityVerificationState(IdentityVerificationPhase.DOCUMENT_UPLOAD, IdentityVerificationStatus.IN_PROGRESS)); - final List drivingLicenseSubmits = ImmutableList.of( + final List drivingLicenseSubmits = List.of( FileSubmit.createFrom("images/driving_license_mock_front.png", DocumentType.DRIVING_LICENSE, CardSide.FRONT) ); final DocumentSubmitRequest driveLicenseSubmitRequest = createDocumentSubmitRequest(processId, drivingLicenseSubmits); @@ -176,7 +175,7 @@ public static void testSuccessfulIdentityVerificationWithRestarts(final TestCont for (int i = 0; i < 3; i++) { initIdentityVerification(ctx, activationId, processId); - List idCardSubmits = ImmutableList.of( + final List idCardSubmits = List.of( FileSubmit.createFrom("images/id_card_mock_front.png", DocumentType.ID_CARD, CardSide.FRONT), FileSubmit.createFrom("images/id_card_mock_back.png", DocumentType.ID_CARD, CardSide.BACK) ); @@ -190,7 +189,7 @@ public static void testSuccessfulIdentityVerificationWithRestarts(final TestCont assertIdentityVerificationStateWithRetries(ctx, new IdentityVerificationState(IdentityVerificationPhase.DOCUMENT_UPLOAD, IdentityVerificationStatus.IN_PROGRESS)); - final List drivingLicenseSubmits = ImmutableList.of( + final List drivingLicenseSubmits = List.of( FileSubmit.createFrom("images/driving_license_mock_front.png", DocumentType.DRIVING_LICENSE, CardSide.FRONT) ); final DocumentSubmitRequest driveLicenseSubmitRequest = createDocumentSubmitRequest(processId, drivingLicenseSubmits); @@ -227,14 +226,14 @@ public static void testSuccessfulIdentityVerificationMultipleDocSubmits(final Te approveConsent(ctx, processId); initIdentityVerification(ctx, activationId, processId); - List idCardSubmits = ImmutableList.of( + final List idCardSubmits = List.of( FileSubmit.createFrom("images/id_card_mock_front.png", DocumentType.ID_CARD, CardSide.FRONT), FileSubmit.createFrom("images/id_card_mock_back.png", DocumentType.ID_CARD, CardSide.BACK) ); DocumentSubmitRequest idCardSubmitRequest = createDocumentSubmitRequest(processId, idCardSubmits); submitDocuments(ctx, idCardSubmitRequest); - List drivingLicenseSubmits = ImmutableList.of( + final List drivingLicenseSubmits = List.of( FileSubmit.createFrom("images/driving_license_mock_front.png", DocumentType.DRIVING_LICENSE, CardSide.FRONT) ); DocumentSubmitRequest driveLicenseSubmitRequest = createDocumentSubmitRequest(processId, drivingLicenseSubmits); @@ -268,7 +267,7 @@ public static void testDocSubmitDifferentDocumentType(final TestContext ctx) thr approveConsent(ctx, processId); initIdentityVerification(ctx, activationId, processId); - final List docSubmits = ImmutableList.of( + final List docSubmits = List.of( FileSubmit.createFrom("images/id_card_mock_front.png", DocumentType.DRIVING_LICENSE, CardSide.FRONT) ); DocumentSubmitRequest idCardSubmitRequest = createDocumentSubmitRequest(processId, docSubmits); @@ -290,7 +289,7 @@ public static void testDocSubmitDifferentCardSide(final TestContext ctx) throws approveConsent(ctx, processId); initIdentityVerification(ctx, activationId, processId); - List docSubmits = ImmutableList.of( + final List docSubmits = List.of( FileSubmit.createFrom("images/id_card_mock_front.png", DocumentType.ID_CARD, CardSide.BACK) ); DocumentSubmitRequest idCardSubmitRequest = createDocumentSubmitRequest(processId, docSubmits); @@ -312,7 +311,7 @@ public static void testDocSubmitMaxAttemptsLimit(final TestContext ctx) throws E approveConsent(ctx, processId); initIdentityVerification(ctx, activationId, processId); - final List docSubmits = ImmutableList.of( + final List docSubmits = List.of( FileSubmit.createFrom("images/id_card_mock_front.png", DocumentType.DRIVING_LICENSE, CardSide.FRONT) ); @@ -340,7 +339,7 @@ public static void testIdentityVerificationNotDocumentPhotos(final TestContext c approveConsent(ctx, processId); initIdentityVerification(ctx, activationId, processId); - List invalidDocSubmits = ImmutableList.of( + final List invalidDocSubmits = List.of( FileSubmit.createFrom("images/random_photo_1.png", DocumentType.ID_CARD, CardSide.FRONT), FileSubmit.createFrom("images/random_photo_2.png", DocumentType.ID_CARD, CardSide.BACK) ); @@ -360,7 +359,7 @@ public static void testIdentityVerificationCleanup(final TestContext ctx) throws approveConsent(ctx, processId); initIdentityVerification(ctx, activationId, processId); - List idDocSubmits = ImmutableList.of( + final List idDocSubmits = List.of( FileSubmit.createFrom("images/id_card_mock_front.png", DocumentType.ID_CARD, CardSide.FRONT), FileSubmit.createFrom("images/id_card_mock_back.png", DocumentType.ID_CARD, CardSide.BACK) ); @@ -381,7 +380,7 @@ public static void testIdentityVerificationMaxAttemptLimit(final TestContext ctx for (int i = 0; i < 5; i++) { initIdentityVerification(ctx, activationId, processId); - List idDocSubmits = ImmutableList.of( + final List idDocSubmits = List.of( FileSubmit.createFrom("images/id_card_mock_front.png", DocumentType.ID_CARD, CardSide.FRONT), FileSubmit.createFrom("images/id_card_mock_back.png", DocumentType.ID_CARD, CardSide.BACK) ); diff --git a/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/shared/PowerAuthOperationShared.java b/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/shared/PowerAuthOperationShared.java deleted file mode 100644 index d5310297..00000000 --- a/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/shared/PowerAuthOperationShared.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * PowerAuth test and related software components - * Copyright (C) 2023 Wultra s.r.o. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package com.wultra.security.powerauth.test.shared; - -import com.wultra.security.powerauth.client.PowerAuthClient; -import com.wultra.security.powerauth.client.model.enumeration.SignatureType; -import com.wultra.security.powerauth.client.model.error.PowerAuthClientException; -import com.wultra.security.powerauth.client.model.request.OperationApproveRequest; -import com.wultra.security.powerauth.client.model.request.OperationCreateRequest; -import com.wultra.security.powerauth.client.model.request.OperationDetailRequest; -import com.wultra.security.powerauth.client.model.response.OperationDetailResponse; -import com.wultra.security.powerauth.client.model.response.OperationUserActionResponse; -import com.wultra.security.powerauth.configuration.PowerAuthTestConfiguration; - -import java.util.List; - -import static com.wultra.security.powerauth.client.model.enumeration.UserActionResult.APPROVAL_FAILED; -import static com.wultra.security.powerauth.client.model.enumeration.UserActionResult.APPROVED; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -/** - * PowerAuth operations test shared logic. - * - * @author Roman Strobl, roman.strobl@wultra.com - */ -public class PowerAuthOperationShared { - - public static void testOperationApprove(final PowerAuthClient powerAuthClient, final PowerAuthTestConfiguration config, final String version) throws Exception { - final OperationDetailResponse operation = createOperation(powerAuthClient, config, version); - - final OperationApproveRequest approveRequest = createOperationApproveRequest(config, operation.getId(), version); - - final OperationUserActionResponse result = powerAuthClient.operationApprove(approveRequest); - - assertEquals(APPROVED, result.getResult()); - } - - public static void testOperationApproveWithValidProximityOtp(final PowerAuthClient powerAuthClient, final PowerAuthTestConfiguration config, final String version) throws Exception { - final OperationDetailResponse operation = createOperation(powerAuthClient, config, true, version); - - final OperationDetailRequest detailRequest = new OperationDetailRequest(); - detailRequest.setOperationId(operation.getId()); - - final String totp = powerAuthClient.operationDetail(detailRequest).getProximityOtp(); - assertNotNull(totp); - - final OperationApproveRequest approveRequest = createOperationApproveRequest(config, operation.getId(), version); - approveRequest.getAdditionalData().put("proximity_otp", totp); - - final OperationUserActionResponse result = powerAuthClient.operationApprove(approveRequest); - - assertEquals(APPROVED, result.getResult()); - } - - public static void testOperationApproveWithInvalidProximityOtp(final PowerAuthClient powerAuthClient, final PowerAuthTestConfiguration config, final String version) throws Exception { - final OperationDetailResponse operation = createOperation(powerAuthClient, config, true, version); - - final OperationDetailRequest detailRequest = new OperationDetailRequest(); - detailRequest.setOperationId(operation.getId()); - - final String totp = powerAuthClient.operationDetail(detailRequest).getProximityOtp(); - assertNotNull(totp); - - final OperationApproveRequest approveRequest = createOperationApproveRequest(config, operation.getId(), version); - approveRequest.getAdditionalData().put("proximity_otp", "1111"); // invalid otp on purpose, it is too short - - final OperationUserActionResponse result = powerAuthClient.operationApprove(approveRequest); - - assertEquals(APPROVAL_FAILED, result.getResult()); - } - - private static OperationApproveRequest createOperationApproveRequest(final PowerAuthTestConfiguration config, final String operationId, final String version) { - final OperationApproveRequest approveRequest = new OperationApproveRequest(); - approveRequest.setOperationId(operationId); - approveRequest.setUserId(config.getUser(version)); - approveRequest.setApplicationId(config.getApplicationId()); - approveRequest.setData("A2"); - approveRequest.setSignatureType(SignatureType.POSSESSION_KNOWLEDGE); - return approveRequest; - } - - private static OperationDetailResponse createOperation(final PowerAuthClient powerAuthClient, final PowerAuthTestConfiguration config, String version) throws PowerAuthClientException { - return createOperation(powerAuthClient, config, null, version); - } - - private static OperationDetailResponse createOperation(final PowerAuthClient powerAuthClient, final PowerAuthTestConfiguration config, final Boolean proximityCheckEnabled, final String version) throws PowerAuthClientException { - final OperationCreateRequest createRequest = new OperationCreateRequest(); - createRequest.setApplications(List.of(config.getApplicationName())); - createRequest.setUserId(config.getUser(version)); - createRequest.setTemplateName(config.getLoginOperationTemplateName()); - createRequest.setProximityCheckEnabled(proximityCheckEnabled); - - return powerAuthClient.createOperation(createRequest); - } -} diff --git a/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/shared/PowerAuthSignatureShared.java b/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/shared/PowerAuthSignatureShared.java index 8b9a8b84..721a9d07 100644 --- a/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/shared/PowerAuthSignatureShared.java +++ b/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/shared/PowerAuthSignatureShared.java @@ -749,7 +749,7 @@ private static void testSignatureOfflinePersonalizedProximityCheck(final PowerAu final CreatePersonalizedOfflineSignaturePayloadRequest request = new CreatePersonalizedOfflineSignaturePayloadRequest(); request.setActivationId(config.getActivationId(version)); request.setData(offlineData); - request.setProximityCheck(new CreatePersonalizedOfflineSignaturePayloadRequest.ProximityCheck()); + request.setProximityCheck(new CreatePersonalizedOfflineSignaturePayloadRequest.CreateProximityCheck()); request.getProximityCheck().setSeed(seed); request.getProximityCheck().setStepLength(30); @@ -808,7 +808,7 @@ private static void testSignatureOfflinePersonalizedProximityCheck(final PowerAu verifyRequest.setData(signatureBaseString); verifyRequest.setSignature(signature); verifyRequest.setAllowBiometry(true); - verifyRequest.setProximityCheck(new VerifyOfflineSignatureRequest.ProximityCheck()); + verifyRequest.setProximityCheck(new VerifyOfflineSignatureRequest.VerifyProximityCheck()); verifyRequest.getProximityCheck().setSeed(expectedResult ? seed : "bGlnaHQgd28="); verifyRequest.getProximityCheck().setStepLength(30); verifyRequest.getProximityCheck().setStepCount(2); diff --git a/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/v3x/PowerAuthApiTest.java b/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/v3x/PowerAuthApiTest.java index 2ec21ce0..5e718731 100644 --- a/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/v3x/PowerAuthApiTest.java +++ b/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/v3x/PowerAuthApiTest.java @@ -60,158 +60,29 @@ public void setPowerAuthTestConfiguration(PowerAuthTestConfiguration config) { this.config = config; } - @Test - void systemStatusTest() throws PowerAuthClientException { - PowerAuthApiShared.systemStatusTest(powerAuthClient); - } - - @Test - void errorListTest() throws PowerAuthClientException { - PowerAuthApiShared.errorListTest(powerAuthClient); - } - - @Test - void initActivationTest() throws PowerAuthClientException { - PowerAuthApiShared.initActivationTest(powerAuthClient, config, VERSION); - } - - @Test - void prepareActivationTest() throws CryptoProviderException, EncryptorException, IOException, PowerAuthClientException { - PowerAuthApiShared.prepareActivationTest(powerAuthClient, config, VERSION); - } - - @Test - void createActivationTest() throws CryptoProviderException, EncryptorException, IOException, PowerAuthClientException { - PowerAuthApiShared.createActivationTest(powerAuthClient, config, VERSION); - } - - @Test - void updateActivationOtpAndCommitTest() throws CryptoProviderException, EncryptorException, IOException, PowerAuthClientException { - PowerAuthApiShared.updateActivationOtpAndCommitTest(powerAuthClient, config, VERSION); - } - - @Test - void removeActivationTest() throws PowerAuthClientException { - PowerAuthApiShared.removeActivationTest(powerAuthClient, config, VERSION); - } - - @Test - void activationListForUserTest() throws PowerAuthClientException { - PowerAuthApiShared.activationListForUserTest(powerAuthClient, config, VERSION); - } - - @Test - void testGetActivationListForUserPagination() throws PowerAuthClientException { - PowerAuthApiShared.testGetActivationListForUserPagination(powerAuthClient, config, VERSION); - } - - @Test - void lookupActivationsTest() throws PowerAuthClientException { - PowerAuthApiShared.lookupActivationsTest(powerAuthClient, config, VERSION); - } - - @Test - void activationStatusUpdateTest() throws PowerAuthClientException { - PowerAuthApiShared.activationStatusUpdateTest(powerAuthClient, config, VERSION); - } - @Test void verifySignatureTest() throws GenericCryptoException, CryptoProviderException, InvalidKeyException, PowerAuthClientException { PowerAuthApiShared.verifySignatureTest(powerAuthClient, config, VERSION); } - @Test - void nonPersonalizedOfflineSignaturePayloadTest() throws PowerAuthClientException { - PowerAuthApiShared.nonPersonalizedOfflineSignaturePayloadTest(powerAuthClient, config); - } - - @Test - void personalizedOfflineSignaturePayloadTest() throws PowerAuthClientException { - PowerAuthApiShared.personalizedOfflineSignaturePayloadTest(powerAuthClient, config, VERSION); - } - - @Test - void verifyOfflineSignatureTest() throws PowerAuthClientException { - PowerAuthApiShared.verifyOfflineSignatureTest(powerAuthClient, config, VERSION); - } - @Test void unlockVaultAndECDSASignatureTest() throws GenericCryptoException, CryptoProviderException, InvalidKeySpecException, EncryptorException, IOException, InvalidKeyException, PowerAuthClientException { PowerAuthApiShared.unlockVaultAndECDSASignatureTest(powerAuthClient, config, VERSION); } - @Test - void activationHistoryTest() throws PowerAuthClientException { - PowerAuthApiShared.activationHistoryTest(powerAuthClient, config, VERSION); - } - - @Test - void blockAndUnblockActivationTest() throws PowerAuthClientException { - PowerAuthApiShared.blockAndUnblockActivationTest(powerAuthClient, config, VERSION); - } - - @Test - void applicationListTest() throws PowerAuthClientException { - PowerAuthApiShared.applicationListTest(powerAuthClient, config); - } - - @Test - void applicationDetailTest() throws PowerAuthClientException { - PowerAuthApiShared.applicationDetailTest(powerAuthClient, config); - } - - @Test - void applicationVersionLookupTest() throws PowerAuthClientException { - PowerAuthApiShared.applicationVersionLookupTest(powerAuthClient, config); - } - // createApplication and createApplication version tests are skipped to avoid creating too many applications - @Test - void applicationSupportTest() throws PowerAuthClientException { - PowerAuthApiShared.applicationSupportTest(powerAuthClient, config); - } - - @Test - void applicationIntegrationTest() throws PowerAuthClientException { - PowerAuthApiShared.applicationIntegrationTest(powerAuthClient, config); - } - - @Test - void callbackTest() throws PowerAuthClientException { - PowerAuthApiShared.callbackTest(powerAuthClient, config); - } - @Test void createValidateAndRemoveTokenTestActiveActivation() throws InvalidKeySpecException, CryptoProviderException, GenericCryptoException, IOException, EncryptorException, PowerAuthClientException { PowerAuthApiShared.createValidateAndRemoveTokenTestActiveActivation(powerAuthClient, config, VERSION); } - @Test - void createValidateAndRemoveTokenTestBlockedActivation() throws InvalidKeySpecException, CryptoProviderException, GenericCryptoException, IOException, EncryptorException, PowerAuthClientException { - PowerAuthApiShared.createValidateAndRemoveTokenTestBlockedActivation(powerAuthClient, config, VERSION); - } - - @Test - void getEciesDecryptorTest() throws EncryptorException, PowerAuthClientException { - PowerAuthApiShared.getEciesDecryptorTest(powerAuthClient, config, VERSION); - } - - @Test - void recoveryCodeCreateLookupRevokeTest() throws PowerAuthClientException { - PowerAuthApiShared.recoveryCodeCreateLookupRevokeTest(powerAuthClient, config, VERSION); - } @Test void recoveryCodeConfirmAndActivationTest() throws CryptoProviderException, GenericCryptoException, IOException, EncryptorException, InvalidKeyException, InvalidKeySpecException, PowerAuthClientException { PowerAuthApiShared.recoveryCodeConfirmAndActivationTest(powerAuthClient, config, VERSION); } - @Test - void recoveryConfigTest() throws PowerAuthClientException { - PowerAuthApiShared.recoveryConfigTest(powerAuthClient, config); - } - // Activation flags are tested using PowerAuthActivationFlagsTest // Application roles are tested using PowerAuthApplicationRolesTest diff --git a/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/v3x/PowerAuthApplicationRolesTest.java b/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/v3x/PowerAuthApplicationRolesTest.java deleted file mode 100644 index 8f8ba2ed..00000000 --- a/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/v3x/PowerAuthApplicationRolesTest.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * PowerAuth test and related software components - * Copyright (C) 2020 Wultra s.r.o. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package com.wultra.security.powerauth.test.v3x; - -import com.wultra.security.powerauth.client.PowerAuthClient; -import com.wultra.security.powerauth.configuration.PowerAuthTestConfiguration; -import com.wultra.security.powerauth.test.shared.PowerAuthApplicationRolesShared; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -/** - * Application roles tests. - * - * @author Roman Strobl, roman.strobl@wultra.com - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest(classes = PowerAuthTestConfiguration.class) -@EnableConfigurationProperties -class PowerAuthApplicationRolesTest { - - private PowerAuthClient powerAuthClient; - private PowerAuthTestConfiguration config; - - @Autowired - public void setPowerAuthClient(PowerAuthClient powerAuthClient) { - this.powerAuthClient = powerAuthClient; - } - - @Autowired - public void setPowerAuthTestConfiguration(PowerAuthTestConfiguration config) { - this.config = config; - } - - @Test - void applicationRolesCrudTest() throws Exception { - PowerAuthApplicationRolesShared.applicationRolesCrudTest(powerAuthClient, config); - } -} diff --git a/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/v3x/PowerAuthCallbackTest.java b/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/v3x/PowerAuthCallbackTest.java index 6995e77b..444f12cb 100644 --- a/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/v3x/PowerAuthCallbackTest.java +++ b/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/v3x/PowerAuthCallbackTest.java @@ -73,16 +73,6 @@ void tearDown() throws PowerAuthClientException { } } - @Test - void callbackCreateDeleteTest() throws PowerAuthClientException { - PowerAuthCallbackShared.callbackCreateDeleteTest(powerAuthClient, config); - } - - @Test - void callbackUpdateTest() throws PowerAuthClientException { - PowerAuthCallbackShared.callbackUpdateTest(powerAuthClient, config); - } - @Test void callbackExecutionTest() throws PowerAuthClientException, RestClientException { PowerAuthCallbackShared.callbackExecutionTest(powerAuthClient, config, port, VERSION); diff --git a/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/v3x/PowerAuthOperationTest.java b/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/v3x/PowerAuthOperationTest.java deleted file mode 100644 index 063d64fa..00000000 --- a/powerauth-backend-tests/src/test/java/com/wultra/security/powerauth/test/v3x/PowerAuthOperationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * PowerAuth test and related software components - * Copyright (C) 2023 Wultra s.r.o. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package com.wultra.security.powerauth.test.v3x; - -import com.wultra.security.powerauth.client.PowerAuthClient; -import com.wultra.security.powerauth.configuration.PowerAuthTestConfiguration; -import com.wultra.security.powerauth.test.shared.PowerAuthOperationShared; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -/** - * Test of PowerAuth operation endpoints. - * - * @author Lubos Racansky, lubos.racansky@wultra.com - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest(classes = PowerAuthTestConfiguration.class) -@EnableConfigurationProperties -class PowerAuthOperationTest { - - // Test only in the latestPowerAuth protocol version - private static final String VERSION = "3.2"; - - @Autowired - private PowerAuthClient powerAuthClient; - - @Autowired - private PowerAuthTestConfiguration config; - - @Test - void testOperationApprove() throws Exception { - PowerAuthOperationShared.testOperationApprove(powerAuthClient, config, VERSION); - } - - @Test - void testOperationApproveWithValidProximityOtp() throws Exception { - PowerAuthOperationShared.testOperationApproveWithValidProximityOtp(powerAuthClient, config, VERSION); - } - - @Test - void testOperationApproveWithInvalidProximityOtp() throws Exception { - PowerAuthOperationShared.testOperationApproveWithInvalidProximityOtp(powerAuthClient, config, VERSION); - } - -} diff --git a/powerauth-fido2-tests/pom.xml b/powerauth-fido2-tests/pom.xml new file mode 100644 index 00000000..e25e312c --- /dev/null +++ b/powerauth-fido2-tests/pom.xml @@ -0,0 +1,124 @@ + + + 4.0.0 + + powerauth-fido2-tests + PowerAuth FIDO2 Test Web Application + powerauth-fido2-tests + war + + + com.wultra + powerauth-backend-tests-parent + 1.7.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-validation + + + + + io.getlime.security + powerauth-rest-client-spring + ${powerauth-server.version} + + + + + com.webauthn4j + webauthn4j-core + ${webauthn4j.version} + + + + + io.netty + netty-resolver-dns-native-macos + runtime + osx-aarch_64 + + + + + net.logstash.logback + logstash-logback-encoder + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + test-repository + + + !useInternalRepo + + + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + build-info + + build-info + + + + + + + org.projectlombok + lombok + + + + + + + + diff --git a/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/PowerauthFido2TestApplication.java b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/PowerauthFido2TestApplication.java new file mode 100644 index 00000000..b2db5780 --- /dev/null +++ b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/PowerauthFido2TestApplication.java @@ -0,0 +1,36 @@ +/* + * PowerAuth Server and related software components + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.wultra.security.powerauth.fido2; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Spring Boot application main class + * + * @author Jan Pesek, jan.pesek@wultra.com + */ +@SpringBootApplication +public class PowerauthFido2TestApplication { + + public static void main(String[] args) { + SpringApplication.run(PowerauthFido2TestApplication.class, args); + } + +} diff --git a/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/ServletInitializer.java b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/ServletInitializer.java new file mode 100644 index 00000000..bab1f101 --- /dev/null +++ b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/ServletInitializer.java @@ -0,0 +1,36 @@ +/* + * PowerAuth Server and related software components + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.wultra.security.powerauth.fido2; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +/** + * Spring Boot servlet initializer + * + * @author Jan Pesek, jan.pesek@wultra.com + */ +public class ServletInitializer extends SpringBootServletInitializer { + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + return application.sources(PowerauthFido2TestApplication.class); + } + +} diff --git a/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/configuration/PowerAuthConfigProperties.java b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/configuration/PowerAuthConfigProperties.java new file mode 100644 index 00000000..3e4d5e9a --- /dev/null +++ b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/configuration/PowerAuthConfigProperties.java @@ -0,0 +1,40 @@ +/* + * PowerAuth test and related software components + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.wultra.security.powerauth.fido2.configuration; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * PowerAuth clients configuration properties. + * + * @author Jan Pesek, jan.pesek@wultra.com + */ +@Configuration +@ConfigurationProperties(prefix = "powerauth.service") +@Data +public class PowerAuthConfigProperties { + + private String baseUrl; + private SecurityProperties security; + + public record SecurityProperties(String clientToken, String clientSecret) {} + +} diff --git a/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/configuration/PowerAuthWebServiceConfiguration.java b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/configuration/PowerAuthWebServiceConfiguration.java new file mode 100644 index 00000000..0b92e757 --- /dev/null +++ b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/configuration/PowerAuthWebServiceConfiguration.java @@ -0,0 +1,62 @@ +/* + * PowerAuth test and related software components + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.wultra.security.powerauth.fido2.configuration; + +import com.wultra.security.powerauth.client.PowerAuthClient; +import com.wultra.security.powerauth.client.PowerAuthFido2Client; +import com.wultra.security.powerauth.client.model.error.PowerAuthClientException; +import com.wultra.security.powerauth.rest.client.PowerAuthFido2RestClient; +import com.wultra.security.powerauth.rest.client.PowerAuthRestClient; +import com.wultra.security.powerauth.rest.client.PowerAuthRestClientConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +/** + * PowerAuth service configuration class. + * + * @author Jan Pesek, jan.pesek@wultra.com + */ +@Configuration +public class PowerAuthWebServiceConfiguration { + + @Bean + public PowerAuthClient powerAuthClient(final PowerAuthConfigProperties properties) throws PowerAuthClientException { + final String powerAuthServiceUrl = properties.getBaseUrl() + "/rest"; + if (StringUtils.hasText(properties.getSecurity().clientToken())) { + final PowerAuthRestClientConfiguration config = new PowerAuthRestClientConfiguration(); + config.setPowerAuthClientToken(properties.getSecurity().clientToken()); + config.setPowerAuthClientSecret(properties.getSecurity().clientSecret()); + return new PowerAuthRestClient(powerAuthServiceUrl, config); + } + return new PowerAuthRestClient(powerAuthServiceUrl); + } + + @Bean + public PowerAuthFido2Client powerAuthFido2Client(final PowerAuthConfigProperties properties) throws PowerAuthClientException { + if (StringUtils.hasText(properties.getSecurity().clientToken())) { + final PowerAuthRestClientConfiguration config = new PowerAuthRestClientConfiguration(); + config.setPowerAuthClientToken(properties.getSecurity().clientToken()); + config.setPowerAuthClientSecret(properties.getSecurity().clientSecret()); + return new PowerAuthFido2RestClient(properties.getBaseUrl(), config); + } + return new PowerAuthFido2RestClient(properties.getBaseUrl()); + } + +} diff --git a/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/configuration/WebAuthnConfiguration.java b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/configuration/WebAuthnConfiguration.java new file mode 100644 index 00000000..d8bbf579 --- /dev/null +++ b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/configuration/WebAuthnConfiguration.java @@ -0,0 +1,43 @@ +/* + * PowerAuth test and related software components + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.wultra.security.powerauth.fido2.configuration; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.time.Duration; +import java.util.List; + +/** + * WebAuthn configuration properties. + * + * @author Jan Pesek, jan.pesek@wultra.com + */ +@Configuration +@ConfigurationProperties(prefix = "powerauth.webauthn") +@Data +public class WebAuthnConfiguration { + + private String rpId; + private String rpName; + private Duration timeout; + private List allowedOrigins; + +} diff --git a/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/AssertionController.java b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/AssertionController.java new file mode 100644 index 00000000..6eece2be --- /dev/null +++ b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/AssertionController.java @@ -0,0 +1,66 @@ +/* + * PowerAuth test and related software components + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.wultra.security.powerauth.fido2.controller; + +import com.wultra.security.powerauth.client.model.error.PowerAuthClientException; +import com.wultra.security.powerauth.client.model.response.fido2.AssertionVerificationResponse; +import com.wultra.security.powerauth.fido2.controller.request.AssertionOptionsRequest; +import com.wultra.security.powerauth.fido2.controller.request.VerifyAssertionRequest; +import com.wultra.security.powerauth.fido2.controller.response.AssertionOptionsResponse; +import com.wultra.security.powerauth.fido2.service.AssertionService; +import jakarta.servlet.http.HttpSession; +import jakarta.validation.Valid; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * WebAuthn assertion ceremony controller. + * + * @author Jan Pesek, jan.pesek@wultra.com + */ +@Validated +@RestController +@RequestMapping("/assertion") +@AllArgsConstructor +@Slf4j +public class AssertionController { + + final AssertionService assertionService; + + @PostMapping("/options") + public AssertionOptionsResponse options(@Valid @RequestBody final AssertionOptionsRequest request) throws PowerAuthClientException { + return assertionService.assertionOptions(request); + } + + @PostMapping + public AssertionVerificationResponse verify(@Valid @RequestBody final VerifyAssertionRequest request, final HttpSession session) throws PowerAuthClientException { + final AssertionVerificationResponse response = assertionService.authenticate(request); + if (response.isAssertionValid()) { + session.setAttribute("username", response.getUserId()); + session.setAttribute("applicationId", response.getApplicationId()); + } + return response; + } + +} diff --git a/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/DefaultExceptionHandler.java b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/DefaultExceptionHandler.java new file mode 100644 index 00000000..24c7f795 --- /dev/null +++ b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/DefaultExceptionHandler.java @@ -0,0 +1,51 @@ +/* + * PowerAuth test and related software components + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.wultra.security.powerauth.fido2.controller; + +import com.wultra.security.powerauth.client.model.error.PowerAuthError; +import io.getlime.core.rest.model.base.response.ObjectResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; + +/** + * Controller to handle exceptions. + * + * @author Jan Pesek, jan.pesek@wultra.com + */ +@ControllerAdvice +@Slf4j +public class DefaultExceptionHandler { + + @ExceptionHandler + @ResponseStatus(HttpStatus.BAD_REQUEST) + public @ResponseBody ObjectResponse handleErrors(Exception ex) { + logger.error("Error occurred while processing the request: {}", ex.getMessage()); + logger.debug("Exception details:", ex); + final PowerAuthError error = new PowerAuthError(); + error.setCode("ERROR"); + error.setMessage(ex.getMessage()); + error.setLocalizedMessage(ex.getLocalizedMessage()); + return new ObjectResponse<>("ERROR", error); + } + +} diff --git a/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/HomeController.java b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/HomeController.java new file mode 100644 index 00000000..d4274fac --- /dev/null +++ b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/HomeController.java @@ -0,0 +1,94 @@ +/* + * PowerAuth Server and related software components + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.wultra.security.powerauth.fido2.controller; + +import com.wultra.security.powerauth.client.model.error.PowerAuthClientException; +import com.wultra.security.powerauth.fido2.service.Fido2SharedService; +import jakarta.servlet.ServletContext; +import jakarta.servlet.http.HttpSession; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Controller; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; + +import java.util.Map; + +/** + * Controller to display initial web page + * + * @author Jan Pesek, jan.pesek@wultra.com + */ +@Controller +@AllArgsConstructor +@Slf4j +public class HomeController { + + private static final String SESSION_KEY_USERNAME = "username"; + private static final String SESSION_KEY_APPLICATION_ID = "applicationId"; + private static final String REDIRECT_LOGIN = "redirect:login"; + private static final String REDIRECT_PAYMENT = "redirect:payment"; + private static final String LOGIN_PAGE = "login"; + private static final String PAYMENT_PAGE = "payment"; + + private final Fido2SharedService sharedService; + private final ServletContext context; + + @ModelAttribute + public void addAttributes(Map model) { + model.put("servletContextPath", context.getContextPath()); + } + + @GetMapping("/") + public String homePage(Map model, HttpSession session) { + if (StringUtils.hasText((String) session.getAttribute(SESSION_KEY_USERNAME))) { + return REDIRECT_PAYMENT; + } + return REDIRECT_LOGIN; + } + + @GetMapping("/login") + public String loginPage(Map model) throws PowerAuthClientException { + model.put("applications", sharedService.fetchApplicationNameList()); + model.put("templates", sharedService.fetchTemplateNameList()); + return LOGIN_PAGE; + } + + @GetMapping("/payment") + public String profilePage(Map model, HttpSession session) throws PowerAuthClientException { + final String username = (String) session.getAttribute(SESSION_KEY_USERNAME); + final String applicationId = (String) session.getAttribute(SESSION_KEY_APPLICATION_ID); + if (!StringUtils.hasText(username)) { + return REDIRECT_LOGIN; + } + + model.put(SESSION_KEY_USERNAME, username); + model.put(SESSION_KEY_APPLICATION_ID, applicationId); + model.put("templates", sharedService.fetchTemplateNameList()); + return PAYMENT_PAGE; + } + + @GetMapping("/logout") + public String logoutPage(Map model, HttpSession session) { + session.removeAttribute(SESSION_KEY_USERNAME); + return REDIRECT_LOGIN; + } + +} diff --git a/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/RegistrationController.java b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/RegistrationController.java new file mode 100644 index 00000000..ce2e535d --- /dev/null +++ b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/RegistrationController.java @@ -0,0 +1,58 @@ +/* + * PowerAuth test and related software components + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.wultra.security.powerauth.fido2.controller; + +import com.wultra.security.powerauth.client.model.error.PowerAuthClientException; +import com.wultra.security.powerauth.client.model.response.fido2.RegistrationResponse; +import com.wultra.security.powerauth.fido2.controller.request.RegisterCredentialRequest; +import com.wultra.security.powerauth.fido2.controller.request.RegistrationOptionsRequest; +import com.wultra.security.powerauth.fido2.controller.response.RegistrationOptionsResponse; +import com.wultra.security.powerauth.fido2.service.RegistrationService; +import jakarta.validation.Valid; +import lombok.AllArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * WebAuthn registration ceremony controller. + * + * @author Jan Pesek, jan.pesek@wultra.com + */ +@Validated +@RestController +@RequestMapping("/registration") +@AllArgsConstructor +public class RegistrationController { + + private final RegistrationService registrationService; + + @PostMapping("/options") + public RegistrationOptionsResponse options(@Valid @RequestBody final RegistrationOptionsRequest request) throws PowerAuthClientException { + return registrationService.registerOptions(request); + } + + @PostMapping + public RegistrationResponse register(@Valid @RequestBody final RegisterCredentialRequest request) throws PowerAuthClientException { + return registrationService.register(request); + } + +} diff --git a/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/request/AssertionOptionsRequest.java b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/request/AssertionOptionsRequest.java new file mode 100644 index 00000000..ebd79cfc --- /dev/null +++ b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/request/AssertionOptionsRequest.java @@ -0,0 +1,40 @@ +/* + * PowerAuth test and related software components + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.wultra.security.powerauth.fido2.controller.request; + +import jakarta.validation.constraints.NotBlank; + +import java.util.Map; + +/** + * Request for credential assertion options. + * + * @author Jan Pesek, jan.pesek@wultra.com + */ +public record AssertionOptionsRequest ( + String username, + + @NotBlank + String applicationId, + + @NotBlank + String templateName, + + Map operationParameters +) { } diff --git a/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/request/RegisterCredentialRequest.java b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/request/RegisterCredentialRequest.java new file mode 100644 index 00000000..7a5f195e --- /dev/null +++ b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/request/RegisterCredentialRequest.java @@ -0,0 +1,51 @@ +/* + * PowerAuth test and related software components + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.wultra.security.powerauth.fido2.controller.request; + +import com.webauthn4j.data.AuthenticatorAttachment; +import com.webauthn4j.data.PublicKeyCredentialType; +import com.wultra.security.powerauth.client.model.entity.fido2.AuthenticatorAttestationResponse; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * Request for register credentials. + * + * @author Jan Pesek, jan.pesek@wultra.com + */ +public record RegisterCredentialRequest ( + @NotBlank + String applicationId, + + @NotBlank + String username, + + boolean userVerificationRequired, + + @NotBlank + String id, + + @NotNull + PublicKeyCredentialType type, + + AuthenticatorAttachment authenticatorAttachment, + + @NotNull + AuthenticatorAttestationResponse response +) {} diff --git a/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/request/RegistrationOptionsRequest.java b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/request/RegistrationOptionsRequest.java new file mode 100644 index 00000000..dd5726e9 --- /dev/null +++ b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/request/RegistrationOptionsRequest.java @@ -0,0 +1,34 @@ +/* + * PowerAuth test and related software components + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.wultra.security.powerauth.fido2.controller.request; + +import jakarta.validation.constraints.NotBlank; + +/** + * Request for credential registration options. + * + * @author Jan Pesek, jan.pesek@wultra.com + */ +public record RegistrationOptionsRequest( + @NotBlank + String username, + + @NotBlank + String applicationId +) {} diff --git a/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/request/VerifyAssertionRequest.java b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/request/VerifyAssertionRequest.java new file mode 100644 index 00000000..4e323d6f --- /dev/null +++ b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/request/VerifyAssertionRequest.java @@ -0,0 +1,50 @@ +/* + * PowerAuth test and related software components + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.wultra.security.powerauth.fido2.controller.request; + +import com.webauthn4j.data.AuthenticatorAttachment; +import com.webauthn4j.data.PublicKeyCredentialType; +import com.wultra.security.powerauth.client.model.entity.fido2.AuthenticatorAssertionResponse; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * Request for verify credential. + * + * @author Jan Pesek, jan.pesek@wultra.com + */ +public record VerifyAssertionRequest( + @NotBlank + String applicationId, + + @NotBlank + String id, + + @NotNull + PublicKeyCredentialType type, + + AuthenticatorAttachment authenticatorAttachment, + + @NotNull + AuthenticatorAssertionResponse response, + + String expectedChallenge, + + boolean userVerificationRequired +) {} diff --git a/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/response/AssertionOptionsResponse.java b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/response/AssertionOptionsResponse.java new file mode 100644 index 00000000..cb6e4425 --- /dev/null +++ b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/response/AssertionOptionsResponse.java @@ -0,0 +1,38 @@ +/* + * PowerAuth test and related software components + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.wultra.security.powerauth.fido2.controller.response; + +import lombok.Builder; + +import java.util.List; +import java.util.Map; + +/** + * Public key credential assertion options. + * + * @author Jan Pesek, jan.pesek@wultra.com + */ +@Builder +public record AssertionOptionsResponse( + String rpId, + String challenge, + Long timeout, + List allowCredentials, + Map extensions +) {} diff --git a/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/response/CredentialDescriptor.java b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/response/CredentialDescriptor.java new file mode 100644 index 00000000..97c12798 --- /dev/null +++ b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/response/CredentialDescriptor.java @@ -0,0 +1,35 @@ +/* + * PowerAuth test and related software components + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.wultra.security.powerauth.fido2.controller.response; + +import com.webauthn4j.data.AuthenticatorTransport; +import com.webauthn4j.data.PublicKeyCredentialType; + +import java.util.List; + +/** + * Structure used in allowCredentials and excludeCredentials. + * + * @author Jan Pesek, jan.pesek@wultra.com + */ +public record CredentialDescriptor( + PublicKeyCredentialType type, + byte[] id, + List transports +) {} diff --git a/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/response/RegistrationOptionsResponse.java b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/response/RegistrationOptionsResponse.java new file mode 100644 index 00000000..227e0e66 --- /dev/null +++ b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/controller/response/RegistrationOptionsResponse.java @@ -0,0 +1,41 @@ +/* + * PowerAuth test and related software components + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.wultra.security.powerauth.fido2.controller.response; + +import com.webauthn4j.data.PublicKeyCredentialParameters; +import com.webauthn4j.data.PublicKeyCredentialRpEntity; +import com.webauthn4j.data.PublicKeyCredentialUserEntity; +import lombok.Builder; + +import java.util.List; + +/** + * Public key credential creation options. + * + * @author Jan Pesek, jan.pesek@wultra.com + */ +@Builder +public record RegistrationOptionsResponse( + PublicKeyCredentialRpEntity rp, + PublicKeyCredentialUserEntity user, + String challenge, + List pubKeyCredParams, + Long timeout, + List excludeCredentials +) {} diff --git a/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/service/AssertionService.java b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/service/AssertionService.java new file mode 100644 index 00000000..984e5f4d --- /dev/null +++ b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/service/AssertionService.java @@ -0,0 +1,140 @@ +/* + * PowerAuth test and related software components + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.wultra.security.powerauth.fido2.service; + +import com.webauthn4j.data.AuthenticatorTransport; +import com.webauthn4j.data.PublicKeyCredentialType; +import com.wultra.security.powerauth.client.PowerAuthFido2Client; +import com.wultra.security.powerauth.client.model.entity.fido2.AllowCredentials; +import com.wultra.security.powerauth.client.model.error.PowerAuthClientException; +import com.wultra.security.powerauth.client.model.request.fido2.AssertionChallengeRequest; +import com.wultra.security.powerauth.client.model.request.fido2.AssertionVerificationRequest; +import com.wultra.security.powerauth.client.model.response.fido2.AssertionChallengeResponse; +import com.wultra.security.powerauth.client.model.response.fido2.AssertionVerificationResponse; +import com.wultra.security.powerauth.fido2.configuration.WebAuthnConfiguration; +import com.wultra.security.powerauth.fido2.controller.request.AssertionOptionsRequest; +import com.wultra.security.powerauth.fido2.controller.request.VerifyAssertionRequest; +import com.wultra.security.powerauth.fido2.controller.response.AssertionOptionsResponse; +import com.wultra.security.powerauth.fido2.controller.response.CredentialDescriptor; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.*; + +/** + * Service for WebAuthn authentication tasks. + * + * @author Jan Pesek, jan.pesek@wultra.com + */ +@Service +@AllArgsConstructor +@Slf4j +public class AssertionService { + + private final PowerAuthFido2Client fido2Client; + private final WebAuthnConfiguration webAuthNConfig; + + /** + * Build public key assertion options. + * @param request Request form with user input. + * @return Public key assertion options. + * @throws PowerAuthClientException in case of PowerAuth server communication error. + */ + public AssertionOptionsResponse assertionOptions(final AssertionOptionsRequest request) throws PowerAuthClientException { + final String userId = request.username(); + final String applicationId = request.applicationId(); + + logger.info("Building assertion options for userId={}, applicationId={}", userId, applicationId); + + final AssertionChallengeResponse challengeResponse = fetchChallenge(userId, applicationId, request.templateName(), request.operationParameters()); + final var credentialList = Optional.ofNullable(challengeResponse.getAllowCredentials()); + if (credentialList.isEmpty() && StringUtils.hasText(userId)) { + logger.info("User {} is not yet registered.", userId); + throw new IllegalStateException("Not registered yet."); + } + + final List existingCredentials = credentialList + .orElse(Collections.emptyList()) + .stream() + .map(AssertionService::toCredentialDescriptor) + .toList(); + + return AssertionOptionsResponse.builder() + .challenge(challengeResponse.getChallenge()) + .rpId(webAuthNConfig.getRpId()) + .timeout(webAuthNConfig.getTimeout().toMillis()) + .allowCredentials(existingCredentials) + .extensions(Collections.emptyMap()).build(); + } + + /** + * Verify credential at PowerAuth server. + * @param credential Received public key credential. + * @return PowerAuth authentication response. + * @throws PowerAuthClientException in case of PowerAuth server communication error. + */ + public AssertionVerificationResponse authenticate(final VerifyAssertionRequest credential) throws PowerAuthClientException { + final byte[] credentialId = Base64.getUrlDecoder().decode(credential.id()); + + final AssertionVerificationRequest request = new AssertionVerificationRequest(); + request.setCredentialId(Base64.getEncoder().encodeToString(credentialId)); + request.setType(credential.type().getValue()); + if (credential.authenticatorAttachment() != null) { + request.setAuthenticatorAttachment(credential.authenticatorAttachment().getValue()); + } + request.setResponse(credential.response()); + request.setApplicationId(credential.applicationId()); + request.setExpectedChallenge(credential.expectedChallenge()); + request.setRelyingPartyId(webAuthNConfig.getRpId()); + request.setAllowedOrigins(webAuthNConfig.getAllowedOrigins()); + request.setRequiresUserVerification(credential.userVerificationRequired()); + + final AssertionVerificationResponse response = fido2Client.authenticate(request); + logger.debug("Credential assertion response of userId={}: {}", response.getUserId(), response); + logger.info("Activation ID {} of userId={}: valid={}", response.getActivationId(), response.getUserId(), response.isAssertionValid()); + + return response; + } + + private AssertionChallengeResponse fetchChallenge(final String userId, final String applicationId, final String templateName, final Map operationParameters) throws PowerAuthClientException { + logger.info("Getting registration challenge for userId={}, applicationId={}, template={}, parameters={}", userId, applicationId, templateName, operationParameters); + final AssertionChallengeRequest request = new AssertionChallengeRequest(); + if (StringUtils.hasText(userId)) { + request.setUserId(userId); + } + request.setApplicationIds(List.of(applicationId)); + request.setTemplateName(templateName); + if (operationParameters != null) { + request.setParameters(operationParameters); + } + final AssertionChallengeResponse response = fido2Client.requestAssertionChallenge(request); + logger.debug("Assertion challenge response for userId={}: {}", userId, response); + return response; + } + + public static CredentialDescriptor toCredentialDescriptor(final AllowCredentials allowCredentials) { + final List transports = allowCredentials.getTransports().stream() + .map(AuthenticatorTransport::create) + .toList(); + return new CredentialDescriptor(PublicKeyCredentialType.create(allowCredentials.getType()), allowCredentials.getCredentialId(), transports); + } + +} diff --git a/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/service/Fido2SharedService.java b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/service/Fido2SharedService.java new file mode 100644 index 00000000..b518408c --- /dev/null +++ b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/service/Fido2SharedService.java @@ -0,0 +1,105 @@ +/* + * PowerAuth test and related software components + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.wultra.security.powerauth.fido2.service; + +import com.webauthn4j.data.AuthenticatorTransport; +import com.webauthn4j.data.PublicKeyCredentialType; +import com.wultra.security.powerauth.client.PowerAuthClient; +import com.wultra.security.powerauth.client.PowerAuthFido2Client; +import com.wultra.security.powerauth.client.model.entity.Application; +import com.wultra.security.powerauth.client.model.entity.fido2.AuthenticatorDetail; +import com.wultra.security.powerauth.client.model.error.PowerAuthClientException; +import com.wultra.security.powerauth.client.model.response.OperationTemplateDetailResponse; +import com.wultra.security.powerauth.fido2.controller.response.CredentialDescriptor; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.Collections; +import java.util.List; + +/** + * Service shared for registration and authentication. + * + * @author Jan Pesek, jan.pesek@wultra.com + */ +@Service +@AllArgsConstructor +@Slf4j +public class Fido2SharedService { + + private static final String EXTRAS_TRANSPORT_KEY = "transports"; + private static final PublicKeyCredentialType CREDENTIAL_TYPE = PublicKeyCredentialType.PUBLIC_KEY; + + private final PowerAuthFido2Client fido2Client; + private final PowerAuthClient powerAuthClient; + + /** + * Fetch all registered credentials. + * @param userId User to whom the credentials belong. + * @param applicationId Of the used application. + * @return List of credentials. + * @throws PowerAuthClientException if there is an error in PowerAuth communication. + */ + public List fetchExistingCredentials(final String userId, final String applicationId) throws PowerAuthClientException { + if (!StringUtils.hasText(userId) || !StringUtils.hasText(applicationId)) { + return Collections.emptyList(); + } + + return listAuthenticators(userId, applicationId).stream() + .map(Fido2SharedService::toCredentialDescriptor) + .toList(); + } + + /** + * Fetch list of all existing applications. + * @return List of application ids. + * @throws PowerAuthClientException if there is an error in PowerAuth communication. + */ + public List fetchApplicationNameList() throws PowerAuthClientException { + return powerAuthClient.getApplicationList().getApplications() + .stream() + .map(Application::getApplicationId) + .sorted().toList(); + } + + /** + * Fetch all existing operation templates. + * @return List of operation template names. + * @throws PowerAuthClientException if there is an error in PowerAuth communication. + */ + public List fetchTemplateNameList() throws PowerAuthClientException { + return powerAuthClient.operationTemplateList() + .stream() + .map(OperationTemplateDetailResponse::getTemplateName) + .sorted().toList(); + } + + private List listAuthenticators(final String userId, final String applicationId) throws PowerAuthClientException { + return fido2Client.getRegisteredAuthenticatorList(userId, applicationId).getAuthenticators(); + } + + @SuppressWarnings("unchecked") + private static CredentialDescriptor toCredentialDescriptor(final AuthenticatorDetail authenticatorDetail) { + final List transports = (List) authenticatorDetail.getExtras().getOrDefault(EXTRAS_TRANSPORT_KEY, Collections.emptyList()); + return new CredentialDescriptor(CREDENTIAL_TYPE, authenticatorDetail.getCredentialId().getBytes(), transports); + } + +} diff --git a/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/service/RegistrationService.java b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/service/RegistrationService.java new file mode 100644 index 00000000..0d1b06db --- /dev/null +++ b/powerauth-fido2-tests/src/main/java/com/wultra/security/powerauth/fido2/service/RegistrationService.java @@ -0,0 +1,125 @@ +/* + * PowerAuth test and related software components + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.wultra.security.powerauth.fido2.service; + +import com.webauthn4j.data.PublicKeyCredentialParameters; +import com.webauthn4j.data.PublicKeyCredentialRpEntity; +import com.webauthn4j.data.PublicKeyCredentialType; +import com.webauthn4j.data.PublicKeyCredentialUserEntity; +import com.webauthn4j.data.attestation.statement.COSEAlgorithmIdentifier; +import com.wultra.security.powerauth.client.PowerAuthFido2Client; +import com.wultra.security.powerauth.client.model.entity.fido2.AuthenticatorParameters; +import com.wultra.security.powerauth.client.model.error.PowerAuthClientException; +import com.wultra.security.powerauth.client.model.request.fido2.RegistrationRequest; +import com.wultra.security.powerauth.client.model.response.fido2.RegistrationChallengeResponse; +import com.wultra.security.powerauth.client.model.response.fido2.RegistrationResponse; +import com.wultra.security.powerauth.fido2.configuration.WebAuthnConfiguration; +import com.wultra.security.powerauth.fido2.controller.request.RegisterCredentialRequest; +import com.wultra.security.powerauth.fido2.controller.request.RegistrationOptionsRequest; +import com.wultra.security.powerauth.fido2.controller.response.RegistrationOptionsResponse; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Base64; +import java.util.List; + +/** + * Service for WebAuthn registration tasks. + * + * @author Jan Pesek, jan.pesek@wultra.com + */ +@Service +@AllArgsConstructor +@Slf4j +public class RegistrationService { + + private final PowerAuthFido2Client fido2Client; + private final Fido2SharedService fido2SharedService; + private final WebAuthnConfiguration webAuthNConfig; + + /** + * Build public key registration options. + * @param request Request form with user input. + * @return Public key registration options. + * @throws PowerAuthClientException in case of PowerAuth server communication error. + */ + public RegistrationOptionsResponse registerOptions(final RegistrationOptionsRequest request) throws PowerAuthClientException { + final String userId = request.username(); + final String applicationId = request.applicationId(); + + final RegistrationChallengeResponse challengeResponse = fetchChallenge(userId, applicationId); + + logger.info("Building registration options for userId={}, applicationId={}", userId, applicationId); + return RegistrationOptionsResponse.builder() + .rp(new PublicKeyCredentialRpEntity(webAuthNConfig.getRpId(), webAuthNConfig.getRpName())) + .user(new PublicKeyCredentialUserEntity(userId.getBytes(), userId, userId)) + .challenge(challengeResponse.getChallenge()) + .pubKeyCredParams(List.of( + new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, COSEAlgorithmIdentifier.ES256) + )) + .timeout(webAuthNConfig.getTimeout().toMillis()) + .excludeCredentials(fido2SharedService.fetchExistingCredentials(userId, applicationId)) + .build(); + } + + /** + * Register credential at PowerAuth server. + * @param credential Newly created public key credential + * @return PowerAuth registration response. + * @throws PowerAuthClientException in case of PowerAuth server communication error. + */ + public RegistrationResponse register(final RegisterCredentialRequest credential) throws PowerAuthClientException { + logger.info("Registering created credential of userId={}, applicationId={}", credential.username(), credential.applicationId()); + + final RegistrationRequest request = new RegistrationRequest(); + request.setActivationName(credential.username()); + request.setApplicationId(credential.applicationId()); + request.setAuthenticatorParameters(buildAuthenticatorParameters(credential)); + + final RegistrationResponse response = fido2Client.register(request); + logger.debug("Credential registration response of userId={}: {}", credential.username(), response); + logger.info("Activation ID {} of userId={}: status={}", response.getActivationId(), response.getUserId(), response.getActivationStatus()); + return response; + } + + private RegistrationChallengeResponse fetchChallenge(final String userId, final String applicationId) throws PowerAuthClientException { + logger.info("Getting registration challenge for userId={}, applicationId={}", userId, applicationId); + final RegistrationChallengeResponse response = fido2Client.requestRegistrationChallenge(userId, applicationId); + logger.debug("Registration challenge response for userId={}: {}", userId, response); + return response; + } + + private AuthenticatorParameters buildAuthenticatorParameters(final RegisterCredentialRequest credential) { + final byte[] credentialId = Base64.getUrlDecoder().decode(credential.id()); + + final AuthenticatorParameters parameters = new AuthenticatorParameters(); + parameters.setCredentialId(Base64.getEncoder().encodeToString(credentialId)); + if (credential.authenticatorAttachment() != null) { + parameters.setAuthenticatorAttachment(credential.authenticatorAttachment().getValue()); + } + parameters.setType(credential.type().getValue()); + parameters.setResponse(credential.response()); + parameters.setAllowedOrigins(webAuthNConfig.getAllowedOrigins()); + parameters.setRelyingPartyId(webAuthNConfig.getRpId()); + parameters.setRequiresUserVerification(credential.userVerificationRequired()); + return parameters; + } + +} diff --git a/powerauth-fido2-tests/src/main/resources/application-dev.properties b/powerauth-fido2-tests/src/main/resources/application-dev.properties new file mode 100644 index 00000000..c44e0c2b --- /dev/null +++ b/powerauth-fido2-tests/src/main/resources/application-dev.properties @@ -0,0 +1,9 @@ + +# WebAuthn properties configuration +powerauth.webauthn.rpId=localhost +powerauth.webauthn.rpName=Local Development +powerauth.webauthn.allowedOrigins=http://localhost:8083 + +logging.level.com.wultra.*=DEBUG + + diff --git a/powerauth-fido2-tests/src/main/resources/application.properties b/powerauth-fido2-tests/src/main/resources/application.properties new file mode 100644 index 00000000..3d5cd9e1 --- /dev/null +++ b/powerauth-fido2-tests/src/main/resources/application.properties @@ -0,0 +1,19 @@ + +# PowerAuth service configuration +powerauth.service.baseUrl=http://localhost:8080/powerauth-java-server +powerauth.service.security.clientToken= +powerauth.service.security.clientSecret= + +# WebAuthn properties configuration +powerauth.webauthn.rpId= +powerauth.webauthn.rpName= +powerauth.webauthn.timeout=60s +powerauth.webauthn.allowedOrigins= + +# Application Service Configuration +powerauth.fido2.test.service.applicationName=powerauth-fido2-tests +powerauth.fido2.test.service.applicationDisplayName=PowerAuth FIDO2 Test +powerauth.fido2.test.service.applicationEnvironment= + +banner.application.name=${powerauth.fido2.test.service.applicationName} +banner.application.version=@project.version@ diff --git a/powerauth-fido2-tests/src/main/resources/banner.txt b/powerauth-fido2-tests/src/main/resources/banner.txt new file mode 100644 index 00000000..875a0a65 --- /dev/null +++ b/powerauth-fido2-tests/src/main/resources/banner.txt @@ -0,0 +1,8 @@ + ___ _ _ _ ___ ___ ___ ___ ___ _____ _ + | _ \_____ __ _____ _ _ /_\ _ _| |_| |_ | __|_ _| \ / _ \_ ) |_ _|__ __| |_ + | _/ _ \ V V / -_) '_/ _ \ || | _| ' \ | _| | || |) | (_) / / | |/ -_|_-< _| + |_| \___/\_/\_/\___|_|/_/ \_\_,_|\__|_||_| |_| |___|___/ \___/___| |_|\___/__/\__| + +${AnsiColor.GREEN} :: ${banner.application.name} (${banner.application.version}) :: ${AnsiColor.GREEN} +${AnsiColor.RED} :: Spring Boot${spring-boot.formatted-version} :: ${AnsiColor.RED} +${AnsiColor.DEFAULT} diff --git a/powerauth-fido2-tests/src/main/resources/templates/error.html b/powerauth-fido2-tests/src/main/resources/templates/error.html new file mode 100644 index 00000000..fb9cd680 --- /dev/null +++ b/powerauth-fido2-tests/src/main/resources/templates/error.html @@ -0,0 +1,26 @@ + + + + + ERROR + + + +
+ +
+

Error

+
+ +
+

Unexpected error occurred. Is the PowerAuth Server reachable?

+

+
+

+
+
+ +
+ + + \ No newline at end of file diff --git a/powerauth-fido2-tests/src/main/resources/templates/login.html b/powerauth-fido2-tests/src/main/resources/templates/login.html new file mode 100644 index 00000000..570b33fe --- /dev/null +++ b/powerauth-fido2-tests/src/main/resources/templates/login.html @@ -0,0 +1,101 @@ + + + + + Login + + + + + + + + + + + + +
+
+
+ + +
+
+
+ + \ No newline at end of file diff --git a/powerauth-fido2-tests/src/main/resources/templates/payment.html b/powerauth-fido2-tests/src/main/resources/templates/payment.html new file mode 100644 index 00000000..881c096a --- /dev/null +++ b/powerauth-fido2-tests/src/main/resources/templates/payment.html @@ -0,0 +1,85 @@ + + + + + Payment + + + + + + + + + + + + +
+
+
+ + +
+
+
+ + + \ No newline at end of file diff --git a/powerauth-fido2-tests/src/main/webapp/resources/css/wultra-login.css b/powerauth-fido2-tests/src/main/webapp/resources/css/wultra-login.css new file mode 100644 index 00000000..5e523cf3 --- /dev/null +++ b/powerauth-fido2-tests/src/main/webapp/resources/css/wultra-login.css @@ -0,0 +1,83 @@ +.body { + background-image: url("../images/background.png"); + background-position: center center; + background-size: cover; + margin: 0; padding: 0; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +.form-wrapper { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding-top: 120px; + padding-bottom: 120px; + background-color: rgba(5, 55, 81, 0.8); +} + +.form-panel { + max-width: 330px; + margin: 0 auto; + border-radius: 8px; + background-color: rgba(0, 0, 0, 1); +} + +.form-panel .alert .close { + margin-top: -2px !important; +} + +.form-logo { + width: 140px; +} + +.form-logo-wrapper { + margin-top: 10px; + margin-bottom: 20px; + margin-right: 20px; +} + +.form-signin { + padding: 20px; +} + +.form-signin .form-signin-heading, .form-signin .checkbox { + margin-bottom: 10px; + color: #FFF; + text-align: center; +} + +.form-signin .checkbox { + font-weight: normal; +} + +.form-signin .form-control { + position: relative; + height: auto; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 8px; + font-size: 14px; +} + +.form-signin .form-control:focus { + z-index: 2; +} + +.form-signin input[type="text"] { + margin-bottom: -1px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.form-signin input[type="password"] { + margin-bottom: 10px; + border-top-left-radius: 0; + border-top-right-radius: 0; +} \ No newline at end of file diff --git a/powerauth-fido2-tests/src/main/webapp/resources/css/wultra-theme.min.css b/powerauth-fido2-tests/src/main/webapp/resources/css/wultra-theme.min.css new file mode 100644 index 00000000..c8457347 --- /dev/null +++ b/powerauth-fido2-tests/src/main/webapp/resources/css/wultra-theme.min.css @@ -0,0 +1,1448 @@ +/*! Generated by Live LESS Theme Customizer */ +.label,sub,sup{vertical-align:baseline} +body,figure{margin:0} +.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.pre-scrollable{max-height:340px} +html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;font-size:10px;-webkit-tap-highlight-color:transparent} +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} +b,optgroup,strong{font-weight:700} +dfn{font-style:italic} +h1{margin:.67em 0} +mark{background:#ff0;color:#000} +sub,sup{font-size:75%;line-height:0;position:relative} +sup{top:-.5em} +sub{bottom:-.25em} +img{border:0;vertical-align:middle} +svg:not(:root){overflow:hidden} +hr{box-sizing:content-box;height:0} +pre,textarea{overflow:auto} +code,kbd,pre,samp{font-size:1em} +button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0} +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{border:0;padding:0} +input[type=checkbox],input[type=radio]{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-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none} +table{border-collapse:collapse;border-spacing:0} +td,th{padding:0} +@media print{blockquote,img,pre,tr{page-break-inside:avoid} +*,:after,:before{background:0 0!important;color:#000!important;box-shadow:none!important;text-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} +thead{display:table-header-group} +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} +} +.img-thumbnail,body{background-color:#fff} +.btn,.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-warning.active,.btn-warning:active,.btn.active,.btn:active,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover,.form-control,.navbar-toggle,.open>.dropdown-toggle.btn-danger,.open>.dropdown-toggle.btn-default,.open>.dropdown-toggle.btn-info,.open>.dropdown-toggle.btn-primary,.open>.dropdown-toggle.btn-warning{background-image:none} +@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,.glyphicon-btc:before,.glyphicon-xbt:before{content:"\e227"} +.glyphicon-jpy:before,.glyphicon-yen:before{content:"\00a5"} +.glyphicon-rub:before,.glyphicon-ruble: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"} +*,:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box} +body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333} +button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit} +a{color:#2185d5;text-decoration:none} +a:focus,a:hover{color:#175c93;text-decoration:underline} +a:focus{outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px} +.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{padding:4px;line-height:1.42857143;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;display:inline-block;max-width:100%;height:auto} +.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;margin:-1px;padding:0;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} +address,blockquote .small,blockquote footer,blockquote small,dd,dt,pre{line-height:1.42857143} +dt,kbd kbd,label{font-weight:700} +@media (min-width:768px){.lead{font-size:21px} +} +.small,small{font-size:85%} +.mark,mark{background-color:#aa0;padding:.2em} +.list-inline,.list-unstyled{list-style:none;padding-left:0} +.text-left{text-align:left} +.text-right{text-align:right} +.btn,.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:#2185d5} +a.text-primary:focus,a.text-primary:hover{color:#1a69a9} +.text-success{color:#fff} +a.text-success:focus,a.text-success:hover{color:#e6e6e6} +.text-info{color:#fff} +a.text-info:focus,a.text-info:hover{color:#e6e6e6} +.text-warning{color:#fff} +a.text-warning:focus,a.text-warning:hover{color:#e6e6e6} +.text-danger{color:#fff} +a.text-danger:focus,a.text-danger:hover{color:#e6e6e6} +.bg-primary{color:#fff;background-color:#2185d5} +a.bg-primary:focus,a.bg-primary:hover{background-color:#1a69a9} +.bg-success{background-color:#7fc000} +a.bg-success:focus,a.bg-success:hover{background-color:#5d8d00} +.bg-info{background-color:#2185d5} +a.bg-info:focus,a.bg-info:hover{background-color:#1a69a9} +.bg-warning{background-color:#aa0} +a.bg-warning:focus,a.bg-warning:hover{background-color:#770} +.bg-danger{background-color:#c0007f} +a.bg-danger:focus,a.bg-danger:hover{background-color:#8d005d} +pre code,table{background-color:transparent} +.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee} +dl,ol,ul{margin-top:0} +ol,ul{margin-bottom:10px} +ol ol,ol ul,ul ol,ul ul{margin-bottom:0} +.list-inline{margin-left:-5px} +.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px} +dl{margin-bottom:20px} +dd{margin-left:0} +@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;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%;color:#777} +code,pre{color:#333} +blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'} +.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right} +caption,th{text-align:left} +code,kbd{padding:2px 4px;font-size:90%} +.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} +code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace} +code{background-color:#f7f7f7;border-radius:4px} +kbd{color:#fff;background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)} +kbd kbd{padding:0;font-size:100%;box-shadow:none} +pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px} +.container,.container-fluid{margin-right:auto;margin-left:auto} +pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;border-radius:0} +.container,.container-fluid{padding-left:15px;padding-right:15px} +.pre-scrollable{overflow-y:scroll} +@media (min-width:768px){.container{width:750px} +} +@media (min-width:992px){.container{width:970px} +} +@media (min-width:1200px){.container{width:1170px} +} +.row{margin-left:-15px;margin-right:-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-left:15px;padding-right: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} +} +caption{padding-top:8px;padding-bottom:8px;color:#777} +.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,.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,.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 col[class*=col-]{position:static;float:none;display:table-column} +table td[class*=col-],table th[class*=col-]{position:static;float:none;display:table-cell} +.btn-group>.btn-group,.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group,.dropdown-menu{float:left} +.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:#7fc000} +.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:#6ea700} +.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:#2185d5} +.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:#1e77bf} +.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:#aa0} +.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:#909100} +.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:#c0007f} +.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:#a7006e} +.table-responsive{overflow-x:auto;min-height:.01%} +@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,legend{padding:0;border:0} +fieldset{margin:0;min-width:0} +legend{display:block;width:100%;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border-bottom:1px solid #e5e5e5} +label{display:inline-block;max-width:100%;margin-bottom:5px} +input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:none} +input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal} +.form-control,output{display:block;font-size:14px;line-height:1.42857143;color:#555} +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:-webkit-focus-ring-color auto 5px;outline-offset:-2px} +output{padding-top:7px} +.form-control{width:100%;height:34px;padding:6px 12px;background-color:#fff;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,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{border:0;background-color:transparent} +.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} +@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-left:-20px;margin-top:4px\9} +.checkbox+.checkbox,.radio+.radio{margin-top:-5px} +.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:400;cursor:pointer} +.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px} +.checkbox-inline.disabled,.checkbox.disabled label,.radio-inline.disabled,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio label,fieldset[disabled] .radio-inline,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} +.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0;min-height:34px} +.form-control-static.input-lg,.form-control-static.input-sm{padding-left:0;padding-right:0} +.form-group-sm .form-control,.input-sm{font-size:12px;padding:5px 10px;border-radius:3px} +.input-sm{height:30px;line-height:1.5} +select.input-sm{height:30px;line-height:30px} +select[multiple].input-sm,textarea.input-sm{height:auto} +.form-group-sm .form-control{height:30px;line-height:1.5} +.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} +.form-group-lg .form-control,.input-lg{font-size:18px;padding:10px 16px;border-radius:6px} +.input-lg{height:46px;line-height:1.3333333} +select.input-lg{height:46px;line-height:46px} +select[multiple].input-lg,textarea.input-lg{height:auto} +.form-group-lg .form-control{height:46px;line-height:1.3333333} +.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} +.collapsing,.dropdown,.dropup{position:relative} +.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:#fff} +.has-success .form-control{border-color:#fff;-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:#e6e6e6;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #fff;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #fff} +.has-success .input-group-addon{color:#fff;border-color:#fff;background-color:#7fc000} +.has-success .form-control-feedback,.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:#fff} +.has-warning .form-control{border-color:#fff;-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:#e6e6e6;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #fff;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #fff} +.has-warning .input-group-addon{color:#fff;border-color:#fff;background-color:#aa0} +.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,.has-warning .form-control-feedback{color:#fff} +.has-error .form-control{border-color:#fff;-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:#e6e6e6;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #fff;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #fff} +.has-error .input-group-addon{color:#fff;border-color:#fff;background-color:#c0007f} +.has-error .form-control-feedback{color:#fff} +.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-control-static,.form-inline .form-group{display:inline-block} +.form-inline .control-label,.form-inline .form-group{margin-bottom:0;vertical-align:middle} +.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle} +.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 .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{margin-top:0;margin-bottom:0;padding-top:7px} +.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px} +.form-horizontal .form-group{margin-left:-15px;margin-right:-15px} +.form-horizontal .has-feedback .form-control-feedback{right:15px} +@media (min-width:768px){.form-horizontal .control-label{text-align:right;margin-bottom:0;padding-top:7px} +.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px} +.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px} +} +.btn{display:inline-block;margin-bottom:0;font-weight:400;vertical-align:middle;touch-action:manipulation;cursor:pointer;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} +.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px} +.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none} +.btn.active,.btn:active{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;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none} +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.active,.btn-default:active,.btn-default:hover,.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.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:#2185d5;border-color:#1e77bf} +.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#1a69a9;border-color:#0c3251} +.btn-primary.active,.btn-primary:active,.btn-primary:hover,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#1a69a9;border-color:#15568a} +.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:#15568a;border-color:#0c3251} +.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:#2185d5;border-color:#1e77bf} +.btn-primary .badge{color:#2185d5;background-color:#fff} +.btn-success{color:#fff;background-color:#7fc000;border-color:#6ea700} +.btn-success.focus,.btn-success:focus{color:#fff;background-color:#5d8d00;border-color:#1a2700} +.btn-success.active,.btn-success:active,.btn-success:hover,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#5d8d00;border-color:#466900} +.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:#466900;border-color:#1a2700} +.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:#7fc000;border-color:#6ea700} +.btn-success .badge{color:#7fc000;background-color:#fff} +.btn-info{color:#fff;background-color:#2185d5;border-color:#1e77bf} +.btn-info.focus,.btn-info:focus{color:#fff;background-color:#1a69a9;border-color:#0c3251} +.btn-info.active,.btn-info:active,.btn-info:hover,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#1a69a9;border-color:#15568a} +.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:#15568a;border-color:#0c3251} +.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:#2185d5;border-color:#1e77bf} +.btn-info .badge{color:#2185d5;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.active,.btn-warning:active,.btn-warning:hover,.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.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:#c0007f;border-color:#a7006e} +.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#8d005d;border-color:#27001a} +.btn-danger.active,.btn-danger:active,.btn-danger:hover,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#8d005d;border-color:#690046} +.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:#690046;border-color:#27001a} +.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:#c0007f;border-color:#a7006e} +.btn-danger .badge{color:#c0007f;background-color:#fff} +.btn-link{color:#2185d5;font-weight:400;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:#175c93;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{height:0;overflow:hidden;-webkit-transition-property:height,visibility;transition-property:height,visibility;-webkit-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;transition-timing-function:ease} +.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-toggle:focus{outline:0} +.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:14px;text-align:left;background-color:#fff;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);background-clip:padding-box} +.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle,.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0} +.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child,.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0} +.btn-group-vertical>.btn:not(:first-child):not(:last-child),.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn,.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0} +.dropdown-header,.dropdown-menu>li>a{white-space:nowrap;padding:3px 20px;line-height:1.42857143} +.dropdown-menu-right,.dropdown-menu.pull-right{left:auto;right:0} +.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5} +.dropdown-menu>li>a{display:block;clear:both;font-weight:400;color:#333} +.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{text-decoration:none;color:#262626;background-color:#f5f5f5} +.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;outline:0;background-color:#2185d5} +.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;background-color:transparent;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);cursor:not-allowed} +.open>.dropdown-menu{display:block} +.open>a{outline:0} +.dropdown-menu-left{left:0;right:auto} +.dropdown-header{display:block;font-size:12px;color:#777} +.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990} +.nav-justified>.dropdown .dropdown-menu,.nav-tabs.nav-justified>.dropdown .dropdown-menu{left:auto;top:auto} +.pull-right>.dropdown-menu{right:0;left:auto} +.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9;content:""} +.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px} +@media (min-width:768px){.navbar-right .dropdown-menu{left:auto;right:0} +.navbar-right .dropdown-menu-left{left:0;right:auto} +} +.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{margin-left:5px} +.btn .caret,.btn-group>.btn:first-child{margin-left:0} +.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0} +.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px} +.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right: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-lg .caret{border-width:5px 5px 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%} +.media-object.img-thumbnail,.nav>li>a>img{max-width:none} +.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:first-child:not(:last-child){border-radius:4px 4px 0 0} +.btn-group-vertical>.btn:last-child:not(:first-child){border-radius:0 0 4px 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-right-radius:0;border-top-left-radius:0} +.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate} +.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;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-left:0;padding-right: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} +.nav>li,.nav>li>a{position:relative;display:block} +.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-bottom-right-radius:0;border-top-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-bottom-left-radius:0;border-top-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{margin-bottom:0;padding-left:0;list-style:none} +.nav>li>a{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;background-color:transparent;cursor:not-allowed} +.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#2185d5} +.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5} +.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;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default} +.nav-tabs.nav-justified{width:100%;border-bottom:0} +.nav-tabs.nav-justified>li{float:none} +.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px;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{display:table-cell;width:1%} +.nav-tabs.nav-justified>li>a{margin-bottom:0;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-justified>li,.nav-stacked>li{float:none} +.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:#2185d5} +.nav-stacked>li+li{margin-top:2px;margin-left:0} +.nav-justified{width:100%} +.nav-justified>li>a{text-align:center;margin-bottom:5px} +.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-justified>li{display:table-cell;width:1%} +.nav-justified>li>a{margin-bottom:0} +.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-right-radius:0;border-top-left-radius:0} +.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent} +.navbar-collapse{overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch} +.navbar-collapse.in{overflow-y:auto} +@media (min-width:768px){.navbar{border-radius:4px} +.navbar-header{float:left} +.navbar-collapse{width:auto;border-top:0;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-left:0;padding-right:0} +} +.carousel-inner,.embed-responsive,.modal,.modal-open,.progress{overflow:hidden} +@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} +.navbar-static-top{z-index:1000;border-width:0 0 1px} +.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030} +.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;padding:15px;font-size:18px;line-height:20px;height:50px} +.navbar-brand:focus,.navbar-brand:hover{text-decoration:none} +.navbar-brand>img{display:block} +@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-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0} +.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px} +} +.navbar-toggle{position:relative;float:right;margin-right:15px;padding:9px 10px;margin-top:8px;margin-bottom:8px;background-color:transparent;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} +.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;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} +} +.progress-bar-striped,.progress-striped .progress-bar,.progress-striped .progress-bar-danger,.progress-striped .progress-bar-info,.progress-striped .progress-bar-success,.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)} +@media (min-width:768px){.navbar-toggle{display:none} +.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;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);margin:8px -15px} +@media (min-width:768px){.navbar-form .form-control-static,.navbar-form .form-group{display:inline-block} +.navbar-form .control-label,.navbar-form .form-group{margin-bottom:0;vertical-align:middle} +.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle} +.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 .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} +} +.btn .badge,.btn .label{position:relative;top:-1px} +.breadcrumb>li,.pagination{display:inline-block} +@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;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none} +} +.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0} +.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-radius:4px 4px 0 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-left:15px;margin-right:15px} +.navbar-left{float:left!important} +.navbar-right{float:right!important;margin-right:-15px} +.navbar-right~.navbar-right{margin-right:0} +} +.navbar-default{background-color:#fff;border-color:#eee} +.navbar-default .navbar-brand{color:#333} +.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#1a1a1a;background-color:transparent} +.navbar-default .navbar-text{color:#333} +.navbar-default .navbar-nav>li>a{color:#2185d5} +.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:#eee} +.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:#eee} +.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{background-color:#eee;color:#555} +@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#2185d5} +.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:#eee} +.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:#2185d5} +.navbar-default .navbar-link:hover{color:#333} +.navbar-default .btn-link{color:#2185d5} +.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:#333;border-color:#1a1a1a} +.navbar-inverse .navbar-brand{color:#9d9d9d} +.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent} +.navbar-inverse .navbar-nav>li>a,.navbar-inverse .navbar-text{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:#1a1a1a} +.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:#212121} +.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{background-color:#1a1a1a;color:#fff} +@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#1a1a1a} +.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#1a1a1a} +.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:#1a1a1a} +.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+li:before{content:"/\00a0";padding:0 5px;color:#ccc} +.breadcrumb>.active{color:#777} +.pagination{padding-left:0;margin:20px 0;border-radius:4px} +.pager li,.pagination>li{display:inline} +.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.42857143;text-decoration:none;color:#2185d5;background-color:#fff;border:1px solid #ddd;margin-left:-1px} +.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px} +.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px} +.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#175c93;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;background-color:#2185d5;border-color:#2185d5;cursor:default} +.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;background-color:#fff;border-color:#ddd;cursor:not-allowed} +.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-bottom-left-radius:6px;border-top-left-radius:6px} +.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px} +.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5} +.badge,.label{text-align:center;font-weight:700;line-height:1;white-space:nowrap} +.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px} +.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px} +.pager{padding-left:0;margin:20px 0;list-style:none;text-align:center} +.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;background-color:#fff;cursor:not-allowed} +.label{display:inline;padding:.2em .6em .3em;font-size:75%;color:#fff;border-radius:.25em} +a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer} +.label:empty{display:none} +.label-default{background-color:#777} +.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e} +.label-primary{background-color:#2185d5} +.label-primary[href]:focus,.label-primary[href]:hover{background-color:#1a69a9} +.label-success{background-color:#7fc000} +.label-success[href]:focus,.label-success[href]:hover{background-color:#5d8d00} +.label-info{background-color:#2185d5} +.label-info[href]:focus,.label-info[href]:hover{background-color:#1a69a9} +.label-warning{background-color:#f0ad4e} +.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f} +.label-danger{background-color:#c0007f} +.label-danger[href]:focus,.label-danger[href]:hover{background-color:#8d005d} +.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;color:#fff;vertical-align:middle;background-color:#777;border-radius:10px} +.badge:empty{display:none} +.media-object,.thumbnail{display:block} +.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:#2185d5;background-color:#fff} +.jumbotron,.jumbotron .h1,.jumbotron h1{color:inherit} +.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;background-color:#eee} +.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200} +.alert .alert-link,.close{font-weight:700} +.alert,.thumbnail{margin-bottom:20px} +.jumbotron>hr{border-top-color:#d5d5d5} +.container .jumbotron,.container-fluid .jumbotron{border-radius:6px;padding-left:15px;padding-right:15px} +.jumbotron .container{max-width:100%} +@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px} +.container .jumbotron,.container-fluid .jumbotron{padding-left:60px;padding-right:60px} +.jumbotron .h1,.jumbotron h1{font-size:63px} +} +.thumbnail{padding:4px;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-left:auto;margin-right:auto} +a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#2185d5} +.thumbnail .caption{padding:9px;color:#333} +.alert{padding:15px;border:1px solid transparent;border-radius:4px} +.alert h4{margin-top:0;color:inherit} +.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} +.modal,.modal-backdrop{right:0;bottom:0;left:0} +.alert-success{background-color:#7fc000;border-color:#8aa700;color:#fff} +.alert-success hr{border-top-color:#758d00} +.alert-success .alert-link{color:#e6e6e6} +.alert-info{background-color:#2185d5;border-color:#1c8bb6;color:#fff} +.alert-info hr{border-top-color:#197aa0} +.alert-info .alert-link{color:#e6e6e6} +.alert-warning{background-color:#aa0;border-color:#917800;color:#fff} +.alert-warning hr{border-top-color:#776300} +.alert-warning .alert-link{color:#e6e6e6} +.alert-danger{background-color:#c0007f;border-color:#a7008a;color:#fff} +.alert-danger hr{border-top-color:#8d0075} +.alert-danger .alert-link{color:#e6e6e6} +@-webkit-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;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:#2185d5;-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:-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);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:#7fc000} +.progress-striped .progress-bar-success{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:#2185d5} +.progress-striped .progress-bar-info{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:-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:#c0007f} +.progress-striped .progress-bar-danger{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{zoom:1;overflow:hidden} +.media-body{width:10000px} +.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{margin-bottom:20px;padding-left:0} +.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-right-radius:4px;border-top-left-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{text-decoration:none;color:#555;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{background-color:#eee;color:#777;cursor:not-allowed} +.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:#2185d5;border-color:#2185d5} +.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:#cbe3f7} +.list-group-item-success{color:#fff;background-color:#7fc000} +a.list-group-item-success,button.list-group-item-success{color:#fff} +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:#fff;background-color:#6ea700} +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:#fff;border-color:#fff} +.list-group-item-info{color:#fff;background-color:#2185d5} +a.list-group-item-info,button.list-group-item-info{color:#fff} +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:#fff;background-color:#1e77bf} +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:#fff;border-color:#fff} +.list-group-item-warning{color:#fff;background-color:#aa0} +a.list-group-item-warning,button.list-group-item-warning{color:#fff} +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:#fff;background-color:#909100} +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:#fff;border-color:#fff} +.list-group-item-danger{color:#fff;background-color:#c0007f} +a.list-group-item-danger,button.list-group-item-danger{color:#fff} +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:#fff;background-color:#a7006e} +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:#fff;border-color:#fff} +.panel-heading>.dropdown .dropdown-toggle,.panel-title,.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit} +.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-title,.panel>.list-group,.panel>.panel-collapse>.list-group,.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0} +.panel-body{padding:15px} +.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px} +.panel-group .panel-heading,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.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-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>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,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0} +.panel-title{margin-top:0;font-size:16px} +.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 .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0} +.panel>.table-responsive:last-child>.table:last-child,.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,.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>.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-right-radius:3px;border-top-left-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-right-radius:0;border-top-left-radius:0} +.panel>.table-responsive:first-child>.table:first-child,.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,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-right-radius:3px;border-top-left-radius:3px} +.list-group+.panel-footer,.panel-heading+.list-group .list-group-item:first-child{border-top-width:0} +.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-left:15px;padding-right:15px} +.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>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-responsive{border:0;margin-bottom: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+.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:#2185d5} +.panel-primary>.panel-heading{color:#fff;background-color:#2185d5;border-color:#2185d5} +.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#2185d5} +.panel-primary>.panel-heading .badge{color:#2185d5;background-color:#fff} +.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#2185d5} +.panel-success{border-color:#8aa700} +.panel-success>.panel-heading{color:#fff;background-color:#7fc000;border-color:#8aa700} +.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#8aa700} +.panel-success>.panel-heading .badge{color:#7fc000;background-color:#fff} +.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#8aa700} +.panel-info{border-color:#1c8bb6} +.panel-info>.panel-heading{color:#fff;background-color:#2185d5;border-color:#1c8bb6} +.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#1c8bb6} +.panel-info>.panel-heading .badge{color:#2185d5;background-color:#fff} +.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#1c8bb6} +.panel-warning{border-color:#917800} +.panel-warning>.panel-heading{color:#fff;background-color:#aa0;border-color:#917800} +.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#917800} +.panel-warning>.panel-heading .badge{color:#aa0;background-color:#fff} +.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#917800} +.panel-danger{border-color:#a7008a} +.panel-danger>.panel-heading{color:#fff;background-color:#c0007f;border-color:#a7008a} +.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#a7008a} +.panel-danger>.panel-heading .badge{color:#c0007f;background-color:#fff} +.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#a7008a} +.embed-responsive{position:relative;display:block;height:0;padding:0} +.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;left:0;bottom:0;height:100%;width: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;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)} +.popover,.tooltip{text-decoration:none;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;letter-spacing:normal;line-break:auto;line-height:1.42857143;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;word-wrap:normal} +.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)} +button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none} +.modal-content,.popover{background-clip:padding-box} +.modal{display:none;position:fixed;top:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0} +.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out} +.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;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);outline:0} +.modal-backdrop{position:fixed;top:0;z-index:1040;background-color:#000} +.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)} +.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)} +.modal-header{padding:15px;border-bottom:1px solid #e5e5e5} +.tooltip.bottom .tooltip-arrow,.tooltip.bottom-left .tooltip-arrow,.tooltip.bottom-right .tooltip-arrow{top:0;border-width:0 5px 5px;border-bottom-color:#000} +.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-left:5px;margin-bottom:0} +.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} +} +.tooltip.top-left .tooltip-arrow,.tooltip.top-right .tooltip-arrow{bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000} +@media (min-width:992px){.modal-lg{width:900px} +} +.tooltip{position:absolute;z-index:1070;display:block;text-align:left;text-align:start;font-size:12px;opacity:0;filter:alpha(opacity=0)} +.tooltip.in{opacity:.9;filter:alpha(opacity=90)} +.tooltip.top{margin-top:-3px;padding:5px 0} +.tooltip.right{margin-left:3px;padding:0 5px} +.tooltip.bottom{margin-top:3px;padding:5px 0} +.tooltip.left{margin-left:-3px;padding:0 5px} +.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} +.tooltip.top-right .tooltip-arrow{left:5px} +.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{left:50%;margin-left:-5px} +.tooltip.bottom-left .tooltip-arrow{right:5px;margin-top:-5px} +.tooltip.bottom-right .tooltip-arrow{left:5px;margin-top:-5px} +.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;text-align:left;text-align:start;font-size:14px;background-color:#fff;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)} +.carousel-caption,.carousel-control{color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.6)} +.popover.top{margin-top:-10px} +.popover.right{margin-left:10px} +.popover.bottom{margin-top:10px} +.popover.left{margin-left:-10px} +.popover-title{margin:0;padding:8px 14px;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} +.carousel,.carousel-inner{position:relative} +.popover>.arrow{border-width:11px} +.popover>.arrow:after{border-width:10px;content:""} +.popover.top>.arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#999;border-top-color:rgba(0,0,0,.25);bottom:-11px} +.popover.top>.arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#fff} +.popover.left>.arrow:after,.popover.right>.arrow:after{content:" ";bottom:-10px} +.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#999;border-right-color:rgba(0,0,0,.25)} +.popover.right>.arrow:after{left:1px;border-left-width:0;border-right-color:#fff} +.popover.bottom>.arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25);top:-11px} +.popover.bottom>.arrow:after{content:" ";top:1px;margin-left:-10px;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;border-right-width:0;border-left-color:#fff} +.carousel-inner{width:100%} +.carousel-inner>.item{display:none;position:relative;-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;-moz-transition:-moz-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;-moz-perspective:1000px;perspective:1000px} +.carousel-inner>.item.active.right,.carousel-inner>.item.next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);left:0} +.carousel-inner>.item.active.left,.carousel-inner>.item.prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);left:0} +.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);left: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;left:0;bottom:0;width:15%;opacity:.5;filter:alpha(opacity=50);font-size:20px;text-align:center;background-color:transparent} +.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:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1)} +.carousel-control.right{left:auto;right:0;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:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1)} +.carousel-control:focus,.carousel-control:hover{outline:0;color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)} +.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;margin-top:-10px;z-index:5;display:inline-block} +.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;line-height:1;font-family:serif} +.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%;margin-left:-30%;padding-left:0;list-style:none;text-align:center} +.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;border:1px solid #fff;border-radius:10px;cursor:pointer;background-color:#000\9;background-color:transparent} +.carousel-indicators .active{margin:0;width:12px;height:12px;background-color:#fff} +.carousel-caption{position:absolute;left:15%;right:15%;bottom:20px;z-index:10;padding-top:20px;padding-bottom:20px;text-align:center} +.carousel-caption .btn,.text-hide{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{left:20%;right: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{content:" ";display:table} +.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-left:auto;margin-right:auto} +.pull-right{float:right!important} +.pull-left{float:left!important} +.hide{display:none!important} +.show{display:block!important} +.hidden,.visible-lg,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important} +.invisible{visibility:hidden} +.text-hide{font:0/0 a;color:transparent;background-color:transparent;border:0} +.affix{position:fixed} +@-ms-viewport{width:device-width} +@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} +.visible-xs-block{display:block!important} +.visible-xs-inline{display:inline!important} +.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} +.visible-sm-block{display:block!important} +.visible-sm-inline{display:inline!important} +.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} +.visible-md-block{display:block!important} +.visible-md-inline{display:inline!important} +.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} +.visible-lg-block{display:block!important} +.visible-lg-inline{display:inline!important} +.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} +.hidden-print{display:none!important} +} \ No newline at end of file diff --git a/powerauth-fido2-tests/src/main/webapp/resources/images/background.png b/powerauth-fido2-tests/src/main/webapp/resources/images/background.png new file mode 100644 index 00000000..ace947af Binary files /dev/null and b/powerauth-fido2-tests/src/main/webapp/resources/images/background.png differ diff --git a/powerauth-fido2-tests/src/main/webapp/resources/images/favicon.png b/powerauth-fido2-tests/src/main/webapp/resources/images/favicon.png new file mode 100644 index 00000000..d5b44dd7 Binary files /dev/null and b/powerauth-fido2-tests/src/main/webapp/resources/images/favicon.png differ diff --git a/powerauth-fido2-tests/src/main/webapp/resources/images/logo.png b/powerauth-fido2-tests/src/main/webapp/resources/images/logo.png new file mode 100644 index 00000000..a0d30571 Binary files /dev/null and b/powerauth-fido2-tests/src/main/webapp/resources/images/logo.png differ diff --git a/powerauth-fido2-tests/src/main/webapp/resources/js/jquery.min.js b/powerauth-fido2-tests/src/main/webapp/resources/js/jquery.min.js new file mode 100644 index 00000000..200b54e4 --- /dev/null +++ b/powerauth-fido2-tests/src/main/webapp/resources/js/jquery.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0 o.value); + if (operationTemplateList.length < 1) { + console.log("There is no operation template to choose from."); + $("#errorMessage").html("Create a login template first."); + $("#errorDiv").show(); + $(":submit").attr("disabled", true); + } else if (!operationTemplateList.includes("login")) { + console.log("There is not operation template 'login'."); + $('#settingsBlock').show(); + } + + // Check if any application is available to select + const n_applications = $('#applicationId option').toArray().length; + if (n_applications < 2) { + console.log("There is no application to choose from."); + $("#errorMessage").html("Create an application first."); + $("#errorDiv").show(); + $(":submit").attr("disabled", true); + } + + // Set action on Register button click + $('#registerBtn').click(function(){ + $("#username").prop('required', true); + CEREMONY = REGISTRATION_CEREMONY; + }); + + // Set action on Login button click + $('#loginBtn').click(function(){ + $("#username").prop('required', false); + CEREMONY = AUTHENTICATION_CEREMONY; + }); + + // Set action on WebAuthn Settings button click + $('#settingsBtn').click(function () { + const settingsBlock = $('#settingsBlock'); + if (settingsBlock.is(":visible")) { + settingsBlock.hide(); + } else { + settingsBlock.show(); + } + }); + +}); \ No newline at end of file diff --git a/powerauth-fido2-tests/src/main/webapp/resources/js/payment.js b/powerauth-fido2-tests/src/main/webapp/resources/js/payment.js new file mode 100644 index 00000000..4fb39429 --- /dev/null +++ b/powerauth-fido2-tests/src/main/webapp/resources/js/payment.js @@ -0,0 +1,101 @@ + +const operationParamKeys = [] +const operationParamValues = [] + +/** + * Action taken on payment action. + * Shows success or errors to UI. + */ +async function handlePaymentSubmit() { + + const successDiv = $("#successDiv"); + const errorDiv = $("#errorDiv"); + errorDiv.hide(); + successDiv.hide(); + + const username = $("#username").val(); + const applicationId = $("#applicationId").val(); + const templateName = $("#operationTemplate").val(); + let operationParameters = { + "amount": $("#amount").val(), + "currency": $("#currency").val(), + "iban": $("#iban").val(), + } + + for (let i = 0; i < operationParamKeys.length; ++i) { + operationParameters[operationParamKeys[i].value] = operationParamValues[i].value + } + + try { + await requestCredential(username, applicationId, templateName, operationParameters); + successDiv.show(); + + } catch (e) { + errorDiv.show() + $("#errorMessage").html(e.message); + console.log("Error occurred during a ceremony") + console.log(e); + } +} + +/** + * Create additional Operation parameter fields. + */ +function createOperationParameter() { + const formFields = $("#divFormFields"); + const count = operationParamKeys.length; + + const key = document.createElement("input"); + key.type = "text"; + key.id = "key" + count; + key.placeholder = "Key"; + key.class = "form-control"; + key.style.width = "50%"; + + const value = document.createElement("input"); + value.type = "text"; + value.id = "value" + count; + value.placeholder = "Value"; + value.class = "form-control"; + value.style.width = "50%"; + + const div = document.createElement("div"); + div.class = "form-group input-group"; + div.style.width = "100%"; + + operationParamKeys[count] = key; + operationParamValues[count] = value; + + div.append(key); + div.append(value); + formFields.append(div); +} + +$(function() { + + const operationTemplateList = $('#operationTemplate option').toArray().map(o => o.value); + if (operationTemplateList.length < 1) { + console.log("There is no operation template to choose from."); + $("#errorMessage").html("Create a payment template first."); + $("#errorDiv").show(); + $(":submit").attr("disabled", true); + } else if (!operationTemplateList.includes("payment")) { + console.log("There is not operation template 'payment'."); + } + + // Set action on Register button click + $('#payBtn').click(function(){ + CEREMONY = AUTHENTICATION_CEREMONY; + }); + + // Set action on Logout button click + $('#logoutBtn').click(function(){ + window.location.href = SERVLET_CONTEXT_PATH + "/logout"; + }); + + // Set action on Add operation parameter button click + $('#addFieldBtn').click(function(){ + createOperationParameter(); + }); + +}); diff --git a/powerauth-fido2-tests/src/main/webapp/resources/js/webauthn.js b/powerauth-fido2-tests/src/main/webapp/resources/js/webauthn.js new file mode 100644 index 00000000..34bf0468 --- /dev/null +++ b/powerauth-fido2-tests/src/main/webapp/resources/js/webauthn.js @@ -0,0 +1,264 @@ + +const REGISTRATION_CEREMONY = "registration"; +const AUTHENTICATION_CEREMONY = "authentication"; +let CEREMONY; + +/** + * WebAuthn ceremony to create a new credential on register request. + */ +async function createCredential(username, applicationId) { + const options = await fetchRegistrationOptions(username, applicationId); + const credential = await navigator.credentials.create({ + publicKey: options + }); + console.log("Public Key Credential") + console.log(JSON.stringify(credential, null, 2)); + + const registerResponse = await registerCredentials(username, applicationId, credential) + console.log("PowerAuth Registration Response") + console.log(JSON.stringify(registerResponse, null, 2)); + if (registerResponse.activationStatus !== "ACTIVE") { + throw Error("Registration process failed, activation is not in state 'ACTIVE'."); + } +} + +/** + * WebAuthn ceremony to request an existing credential on login request. + */ +async function requestCredential(username, applicationId, templateName, operationParameters) { + const options = await fetchAssertionOptions(username, applicationId, templateName, operationParameters); + const credential = await navigator.credentials.get({ + publicKey: options + }); + + console.log("Public Key Credential") + console.log(JSON.stringify(credential, null, 2)); + + const authenticateResponse = await verifyAssertion(applicationId, options.challenge, credential) + console.log("PowerAuth Authentication Response") + console.log(JSON.stringify(authenticateResponse, null, 2)); + if (!authenticateResponse.assertionValid) { + throw Error("Assertion is not valid."); + } +} + +/** + * Fetch and return public key credential creation options. + * @param username Username from user input. + * @param applicationId Application ID from user input. + * @returns public key credential creation options + */ +async function fetchRegistrationOptions(username, applicationId) { + + const fetchOptionsRequest = { + "username": username, + "applicationId": applicationId + }; + + let options = await post("/registration/options", fetchOptionsRequest) + + // Build selection criteria from WebAuthn settings customisable from UI + const userVerification = $("#userVerification").val(); + const residentKey = $("#residentKey").val(); + const authenticatorAttachment = $("#authenticatorAttachment").val(); + let authenticatorSelection = { + userVerification: userVerification, + residentKey: residentKey, + requireResidentKey: residentKey === "required", + } + + // If user did not choose platform or cross-platform attachment, omit the field to allow both. + if (authenticatorAttachment === "platform" || authenticatorAttachment === "cross-platform") { + authenticatorSelection = { + ...authenticatorSelection, + authenticatorAttachment: authenticatorAttachment + } + } + + // Add WebAuthn settings customisable from UI + options = { + ...options, + authenticatorSelection: authenticatorSelection, + attestation: $("#attestation").val() + } + + console.log("Public Key Credential Creation Options") + console.log(JSON.stringify(options, null, 2)); + + // Some fields have to be passed as buffer to navigator.create() + const byteEncoder = new TextEncoder(); + return { + ...options, + challenge: byteEncoder.encode(options.challenge), + user: { + ...options.user, + id: byteEncoder.encode(options.user.id) + }, + excludeCredentials: options.excludeCredentials?.map( credentialDescriptor => ({ + ...credentialDescriptor, + id: toBuffer(credentialDescriptor.id) + }) ) + } + +} + +/** + * Fetch and return public key credential request options. + * @param username Username from user input, may be empty. + * @param applicationId Application ID from user input. + * @param templateName Template name to use. + * @param operationParameters Parameters of the operation. + * @returns public key credential request options + */ +async function fetchAssertionOptions(username, applicationId, templateName, operationParameters) { + + const fetchOptionsRequest = { + "username": username, + "applicationId": applicationId, + "templateName": templateName, + "operationParameters": operationParameters + }; + + let options = await post("/assertion/options", fetchOptionsRequest) + // Add WebAuthn settings customisable from UI + options = { + ...options, + userVerification:$("#userVerification").val(), + } + + console.log("Public Key Credential Request Options") + console.log(JSON.stringify(options, null, 2)); + + // Some fields have to be passed as buffer to navigator.get() + const byteEncoder = new TextEncoder(); + return { + ...options, + challenge: byteEncoder.encode(options.challenge), + allowCredentials: options.allowCredentials?.map( credentialDescriptor => ({ + ...credentialDescriptor, + id: toBuffer(credentialDescriptor.id) + }) ), + extensions: { + ...options.extensions, + } + } +} + +/** + * Send register credential request to PowerAuth Server + * @param username Username from user input. + * @param applicationId Application ID from user input. + * @param credential Newly created credential. + * @returns JSON response from PowerAuth Server. + */ +async function registerCredentials(username, applicationId, credential) { + const userVerification = $("#userVerification").val(); + + // RP entity and allowedOrigins are added on backend level + const requestBody = { + applicationId: applicationId, + username: username, + userVerificationRequired: userVerification === "required", + id: credential.id, + type: credential.type, + authenticatorAttachment: credential.authenticatorAttachment, + response: { + clientDataJSON: toBase64(credential.response.clientDataJSON), + attestationObject: toBase64(credential.response.attestationObject), + transports: credential.response.getTransports() + } + }; + + console.log("PowerAuth Registration Request") + return await post("/registration", requestBody); +} + +/** + * Send verify assertion request to PowerAuth server. + * @param applicationId Application ID from user input. + * @param challenge Challenge received from PowerAuth server. + * @param credential Retrieved credential. + * @returns JSON response from PowerAuth Server. + */ +async function verifyAssertion(applicationId, challenge, credential) { + const decoder = new TextDecoder(); + const requestBody = { + applicationId: applicationId, + id: credential.id, + type: credential.type, + authenticatorAttachment: credential.authenticatorAttachment, + response: { + clientDataJSON: toBase64(credential.response.clientDataJSON), + authenticatorData: toBase64(credential.response.authenticatorData), + signature: toBase64(credential.response.signature), + userHandle: credential.response.userHandle == null ? null : decoder.decode(credential.response.userHandle) + }, + expectedChallenge: decoder.decode(challenge), + userVerificationRequired: $("#userVerification").val() === "required" + }; + + console.log("PowerAuth Assertion Request") + return await post("/assertion", requestBody); +} + +/** + * Send a POST request to backend service. + * @param apiPath API path for the request. + * @param requestBody Body of the request. + * @returns JSON response. + */ +async function post(apiPath, requestBody) { + console.log("POST " + apiPath); + console.log(JSON.stringify(requestBody, null, 2)); + const response = await fetch(SERVLET_CONTEXT_PATH + apiPath, { + method: "POST", + body: JSON.stringify(requestBody), + headers: { + "Content-type": "application/json; charset=UTF-8" + } + }); + + const json = await response.json(); + + if (response.status !== 200) { + if (json.hasOwnProperty("responseObject")) { + throw Error(json.responseObject.message); + } else if (json.hasOwnProperty("error")) { + throw Error(json.error); + } + throw Error(JSON.stringify(json)); + } + + return json; +} + +/** + * Convert buffer to base64 string. + * @param buffer Buffer to convert. + * @returns {string} Converted string. + */ +function toBase64(buffer) { + const byteView = new Uint8Array(buffer); + let str = ""; + for (const charCode of byteView) { + str += String.fromCharCode(charCode); + } + return btoa(str); +} + +/** + * Convert base64 string to buffer. + * @param base64 String to convert. + * @returns {ArrayBufferLike} Converted array. + */ +function toBuffer(base64) { + const binary_string = atob(base64); + const len = binary_string.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binary_string.charCodeAt(i); + } + return bytes.buffer; +} + + diff --git a/powerauth-load-tests/pom.xml b/powerauth-load-tests/pom.xml index c3b12d7b..f61856cf 100644 --- a/powerauth-load-tests/pom.xml +++ b/powerauth-load-tests/pom.xml @@ -6,7 +6,7 @@ com.wultra powerauth-backend-tests-parent - 1.6.0 + 1.7.0 com.wultra @@ -38,13 +38,12 @@ - 2.13.6 - 2.13.6 + 2.13.13 - 4.7.0 + 4.8.2 4.8.1 - 3.10.3 + 3.10.5 @@ -83,7 +82,7 @@ -Xss100M - -target:jvm-1.8 + -release:17 -deprecation -feature -unchecked diff --git a/powerauth-test-server/Dockerfile b/powerauth-test-server/Dockerfile index 68b351ae..dca87162 100644 --- a/powerauth-test-server/Dockerfile +++ b/powerauth-test-server/Dockerfile @@ -1,4 +1,4 @@ -FROM ibm-semeru-runtimes:open-17.0.9_9-jre +FROM ibm-semeru-runtimes:open-21.0.2_13-jre LABEL maintainer="roman.strobl@wultra.com" # Prepare environment variables @@ -8,7 +8,7 @@ ENV JAVA_HOME=/opt/java/openjdk \ LB_VERSION=4.23.2 \ TOMCAT_HOME=/usr/local/tomcat \ TOMCAT_MAJOR=10 \ - TOMCAT_VERSION=10.1.17 \ + TOMCAT_VERSION=10.1.19 \ LOGBACK_CONF=/opt/logback/conf \ TZ=UTC @@ -21,7 +21,7 @@ RUN apt-get -y update \ # Install tomcat RUN curl -jkSL -o /tmp/apache-tomcat.tar.gz http://archive.apache.org/dist/tomcat/tomcat-${TOMCAT_MAJOR}/v${TOMCAT_VERSION}/bin/apache-tomcat-${TOMCAT_VERSION}.tar.gz \ - && [ "ff9670f9cd49a604e47edfbcfb5855fe59342048c3278ea8736276b51327adf2d076973f3ad1b8aa7870ef26c28cf7111527be810b445c9927f2a457795f5cb6 /tmp/apache-tomcat.tar.gz" = "$(sha512sum /tmp/apache-tomcat.tar.gz)" ] \ + && [ "7264da6196a510b0bba74469d215d61a464331302239256477f78b6bec067f7f4d90f671b96a440061ae0e20d16b1be8ca1dbd547dab9927383366dbc677f590 /tmp/apache-tomcat.tar.gz" = "$(sha512sum /tmp/apache-tomcat.tar.gz)" ] \ && gunzip /tmp/apache-tomcat.tar.gz \ && tar -C /opt -xf /tmp/apache-tomcat.tar \ && ln -s /opt/apache-tomcat-$TOMCAT_VERSION $TOMCAT_HOME diff --git a/powerauth-test-server/docker/powerauth-test-server.xml b/powerauth-test-server/docker/powerauth-test-server.xml index d2381332..3c901f65 100644 --- a/powerauth-test-server/docker/powerauth-test-server.xml +++ b/powerauth-test-server/docker/powerauth-test-server.xml @@ -14,7 +14,6 @@ - diff --git a/powerauth-test-server/lombok.config b/powerauth-test-server/lombok.config deleted file mode 100644 index 2bb794fd..00000000 --- a/powerauth-test-server/lombok.config +++ /dev/null @@ -1 +0,0 @@ -lombok.log.fieldName=logger diff --git a/powerauth-test-server/pom.xml b/powerauth-test-server/pom.xml index 81b8326a..f72896e2 100644 --- a/powerauth-test-server/pom.xml +++ b/powerauth-test-server/pom.xml @@ -23,7 +23,7 @@ com.wultra powerauth-backend-tests-parent - 1.6.0 + 1.7.0 powerauth-test-server @@ -145,6 +145,25 @@ ${springdoc-openapi-starter-webmvc-ui.version} + + + io.projectreactor + reactor-core-micrometer + + + + io.micrometer + micrometer-tracing-bridge-otel + + + + + io.netty + netty-resolver-dns-native-macos + runtime + osx-aarch_64 + + diff --git a/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/controller/ActivationController.java b/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/controller/ActivationController.java index f79368b7..8263a9e2 100644 --- a/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/controller/ActivationController.java +++ b/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/controller/ActivationController.java @@ -28,7 +28,9 @@ import io.getlime.core.rest.model.base.request.ObjectRequest; import io.getlime.core.rest.model.base.response.ObjectResponse; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import jakarta.validation.Valid; /** * Controller for activation actions. @@ -37,6 +39,7 @@ */ @RestController @RequestMapping("activation") +@Validated public class ActivationController { private final ActivationService activationService; @@ -60,7 +63,7 @@ public ActivationController(ActivationService activationService) { * @throws ActivationFailedException Thrown when activation fails. */ @PostMapping("create") - public ObjectResponse createActivation(@RequestBody ObjectRequest request) throws AppConfigNotFoundException, GenericCryptographyException, RemoteExecutionException, ActivationFailedException { + public ObjectResponse createActivation(@Valid @RequestBody ObjectRequest request) throws AppConfigNotFoundException, GenericCryptographyException, RemoteExecutionException, ActivationFailedException { // TODO - input validation final CreateActivationResponse response = activationService.createActivation(request.getRequestObject()); return new ObjectResponse<>(response); diff --git a/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/controller/ApplicationController.java b/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/controller/ApplicationController.java index f86c49a2..98f1b597 100644 --- a/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/controller/ApplicationController.java +++ b/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/controller/ApplicationController.java @@ -23,7 +23,9 @@ import com.wultra.security.powerauth.app.testserver.service.ApplicationService; import io.getlime.core.rest.model.base.request.ObjectRequest; import io.getlime.core.rest.model.base.response.Response; +import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; /** @@ -32,6 +34,7 @@ * @author Roman Strobl, roman.strobl@wultra.com */ @RestController +@Validated @RequestMapping("application") public class ApplicationController { @@ -53,7 +56,7 @@ public ApplicationController(ApplicationService applicationService) { * @throws AppConfigInvalidException Thrown in case mobile SDK configuration is invalid. */ @PostMapping("config") - public Response createActivation(@RequestBody ObjectRequest request) throws AppConfigInvalidException { + public Response createActivation(@Valid @RequestBody ObjectRequest request) throws AppConfigInvalidException { return applicationService.configureApplication(request.getRequestObject()); } diff --git a/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/controller/OperationsController.java b/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/controller/OperationsController.java index 45abc814..63b09a2b 100644 --- a/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/controller/OperationsController.java +++ b/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/controller/OperationsController.java @@ -31,8 +31,10 @@ import io.getlime.core.rest.model.base.response.Response; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.enums.ParameterIn; +import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -44,6 +46,7 @@ * @author Petr Dvorak, petr@wultra.com */ @RestController +@Validated @RequestMapping("operations") public class OperationsController { @@ -69,7 +72,7 @@ public OperationsController(OperationsService operationsService) { */ @Parameter(name = HttpHeaders.ACCEPT_LANGUAGE, in = ParameterIn.HEADER, allowEmptyValue = true, description = "Preferred language in which we want to get the operations.", example = "en") @PostMapping("pending") - public ObjectResponse fetchOperations(@RequestBody ObjectRequest request) throws RemoteExecutionException, RestClientException, SignatureVerificationException, ActivationFailedException { + public ObjectResponse fetchOperations(@Valid @RequestBody ObjectRequest request) throws RemoteExecutionException, RestClientException, SignatureVerificationException, ActivationFailedException { final OperationListResponse response = operationsService.getOperations(request.getRequestObject()); return new ObjectResponse<>(response); } @@ -84,7 +87,7 @@ public ObjectResponse fetchOperations(@RequestBody Object * @throws AppConfigNotFoundException In case app configuration is not found. */ @PostMapping("approve") - public Response approveOperations(@RequestBody ObjectRequest request) throws RemoteExecutionException, AppConfigNotFoundException, SignatureVerificationException, ActivationFailedException { + public Response approveOperations(@Valid @RequestBody ObjectRequest request) throws RemoteExecutionException, AppConfigNotFoundException, SignatureVerificationException, ActivationFailedException { return operationsService.approveOperation(request.getRequestObject()); } diff --git a/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/controller/SignatureController.java b/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/controller/SignatureController.java index 2085c3eb..4cff6b09 100644 --- a/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/controller/SignatureController.java +++ b/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/controller/SignatureController.java @@ -27,7 +27,9 @@ import com.wultra.security.powerauth.app.testserver.service.SignatureService; import io.getlime.core.rest.model.base.request.ObjectRequest; import io.getlime.core.rest.model.base.response.ObjectResponse; +import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; /** @@ -36,6 +38,7 @@ * @author Roman Strobl, roman.strobl@wultra.com */ @RestController +@Validated @RequestMapping("signature") public class SignatureController { @@ -59,7 +62,7 @@ public SignatureController(SignatureService signatureService) { * @throws AppConfigNotFoundException In case application configuration is not found. */ @PostMapping("compute-online") - public ObjectResponse computeOnlineSignature(@RequestBody ObjectRequest request) throws RemoteExecutionException, ActivationFailedException, AppConfigNotFoundException { + public ObjectResponse computeOnlineSignature(@Valid @RequestBody ObjectRequest request) throws RemoteExecutionException, ActivationFailedException, AppConfigNotFoundException { final ComputeOnlineSignatureResponse response = signatureService.computeOnlineSignature(request.getRequestObject()); return new ObjectResponse<>(response); } @@ -72,7 +75,7 @@ public ObjectResponse computeOnlineSignature(@Re * @throws ActivationFailedException In case activation is not found. */ @PostMapping("compute-offline") - public ObjectResponse computeOfflineSignature(@RequestBody ObjectRequest request) throws RemoteExecutionException, ActivationFailedException { + public ObjectResponse computeOfflineSignature(@Valid @RequestBody ObjectRequest request) throws RemoteExecutionException, ActivationFailedException { final ComputeOfflineSignatureResponse response = signatureService.computeOfflineSignature(request.getRequestObject()); return new ObjectResponse<>(response); } diff --git a/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/controller/TokenController.java b/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/controller/TokenController.java index 2ab403e4..41b88621 100644 --- a/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/controller/TokenController.java +++ b/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/controller/TokenController.java @@ -28,6 +28,7 @@ import com.wultra.security.powerauth.app.testserver.service.TokenService; import io.getlime.core.rest.model.base.request.ObjectRequest; import io.getlime.core.rest.model.base.response.ObjectResponse; +import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @@ -61,7 +62,7 @@ public TokenController(TokenService tokenService) { * @throws ActivationFailedException In case activation is not found. */ @PostMapping("create") - public ObjectResponse createToken(@RequestBody ObjectRequest request) throws GenericCryptographyException, RemoteExecutionException, AppConfigNotFoundException, ActivationFailedException { + public ObjectResponse createToken(@Valid @RequestBody ObjectRequest request) throws GenericCryptographyException, RemoteExecutionException, AppConfigNotFoundException, ActivationFailedException { final CreateTokenResponse response = tokenService.createToken(request.getRequestObject()); return new ObjectResponse<>(response); } @@ -74,7 +75,7 @@ public ObjectResponse createToken(@RequestBody ObjectReques * @throws ActivationFailedException In case activation is not found. */ @PostMapping("compute-digest") - public ObjectResponse computeTokenDigest(@RequestBody ObjectRequest request) throws RemoteExecutionException, ActivationFailedException { + public ObjectResponse computeTokenDigest(@Valid@RequestBody ObjectRequest request) throws RemoteExecutionException, ActivationFailedException { final ComputeTokenDigestResponse response = tokenService.computeTokenDigest(request.getRequestObject()); return new ObjectResponse<>(response); } diff --git a/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/ComputeOfflineSignatureRequest.java b/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/ComputeOfflineSignatureRequest.java index 41c4d228..cf760e3c 100644 --- a/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/ComputeOfflineSignatureRequest.java +++ b/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/ComputeOfflineSignatureRequest.java @@ -17,6 +17,7 @@ */ package com.wultra.security.powerauth.app.testserver.model.request; +import jakarta.validation.constraints.NotBlank; import lombok.Getter; import lombok.Setter; @@ -28,9 +29,11 @@ @Getter @Setter public class ComputeOfflineSignatureRequest { - + @NotBlank private String activationId; + @NotBlank private String qrCodeData; + @NotBlank private String password; } diff --git a/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/ComputeOnlineSignatureRequest.java b/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/ComputeOnlineSignatureRequest.java index e8dbcb82..f52f62db 100644 --- a/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/ComputeOnlineSignatureRequest.java +++ b/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/ComputeOnlineSignatureRequest.java @@ -17,6 +17,7 @@ */ package com.wultra.security.powerauth.app.testserver.model.request; +import jakarta.validation.constraints.NotBlank; import lombok.Getter; import lombok.Setter; @@ -28,11 +29,15 @@ @Getter @Setter public class ComputeOnlineSignatureRequest { - + @NotBlank private String activationId; + @NotBlank private String applicationId; + @NotBlank private String httpMethod; + @NotBlank private String resourceId; + @NotBlank private String signatureType; private String requestBody; private String password; diff --git a/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/ComputeTokenDigestRequest.java b/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/ComputeTokenDigestRequest.java index aac4e424..f2ae046d 100644 --- a/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/ComputeTokenDigestRequest.java +++ b/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/ComputeTokenDigestRequest.java @@ -17,6 +17,7 @@ */ package com.wultra.security.powerauth.app.testserver.model.request; +import jakarta.validation.constraints.NotBlank; import lombok.Getter; import lombok.Setter; @@ -29,8 +30,11 @@ @Setter public class ComputeTokenDigestRequest { + @NotBlank private String activationId; + @NotBlank private String tokenId; + @NotBlank private String tokenSecret; } diff --git a/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/ConfigureApplicationRequest.java b/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/ConfigureApplicationRequest.java index 39cf971b..e6618b49 100644 --- a/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/ConfigureApplicationRequest.java +++ b/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/ConfigureApplicationRequest.java @@ -18,6 +18,8 @@ package com.wultra.security.powerauth.app.testserver.model.request; +import jakarta.validation.constraints.NotBlank; + import lombok.Getter; import lombok.Setter; @@ -29,8 +31,9 @@ @Getter @Setter public class ConfigureApplicationRequest { - + @NotBlank private String applicationId; + @NotBlank private String applicationName; private String applicationKey; private String applicationSecret; diff --git a/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/CreateActivationRequest.java b/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/CreateActivationRequest.java index f2b6f86b..0ce754bb 100644 --- a/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/CreateActivationRequest.java +++ b/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/CreateActivationRequest.java @@ -18,6 +18,7 @@ package com.wultra.security.powerauth.app.testserver.model.request; +import jakarta.validation.constraints.NotBlank; import lombok.Data; /** @@ -28,6 +29,7 @@ @Data public class CreateActivationRequest { + @NotBlank private String applicationId; private String activationName; private String password; diff --git a/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/CreateTokenRequest.java b/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/CreateTokenRequest.java index 4c20a4d0..ed6e3be6 100644 --- a/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/CreateTokenRequest.java +++ b/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/CreateTokenRequest.java @@ -17,6 +17,7 @@ */ package com.wultra.security.powerauth.app.testserver.model.request; +import jakarta.validation.constraints.NotBlank; import lombok.Data; /** @@ -26,9 +27,11 @@ */ @Data public class CreateTokenRequest { - + @NotBlank private String activationId; + @NotBlank private String applicationId; private String password; + @NotBlank private String signatureType; } diff --git a/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/GetOperationsRequest.java b/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/GetOperationsRequest.java index d71c3bf0..c259199f 100644 --- a/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/GetOperationsRequest.java +++ b/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/GetOperationsRequest.java @@ -17,6 +17,7 @@ */ package com.wultra.security.powerauth.app.testserver.model.request; +import jakarta.validation.constraints.NotBlank; import lombok.Data; /** @@ -26,9 +27,11 @@ */ @Data public class GetOperationsRequest { - + @NotBlank private String activationId; + @NotBlank private String tokenId; + @NotBlank private String tokenSecret; } diff --git a/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/OperationApproveInternalRequest.java b/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/OperationApproveInternalRequest.java index f17d2928..9bbeb496 100644 --- a/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/OperationApproveInternalRequest.java +++ b/powerauth-test-server/src/main/java/com/wultra/security/powerauth/app/testserver/model/request/OperationApproveInternalRequest.java @@ -17,6 +17,7 @@ */ package com.wultra.security.powerauth.app.testserver.model.request; +import jakarta.validation.constraints.NotBlank; import lombok.Data; /** @@ -26,11 +27,14 @@ */ @Data public class OperationApproveInternalRequest { - + @NotBlank private String activationId; + @NotBlank private String applicationId; private String password; + @NotBlank private String operationId; + @NotBlank private String operationData; } diff --git a/powerauth-test-server/src/main/resources/application.properties b/powerauth-test-server/src/main/resources/application.properties index 707d8f9c..5c2a0822 100644 --- a/powerauth-test-server/src/main/resources/application.properties +++ b/powerauth-test-server/src/main/resources/application.properties @@ -13,13 +13,11 @@ resultstatus.persistenceType=memory #spring.datasource.url=jdbc:h2:file:~/powerauth-test;DB_CLOSE_ON_EXIT=FALSE;AUTO_SERVER=TRUE #spring.datasource.username=sa #spring.datasource.password= -#spring.datasource.driver-class-name=org.h2.Driver # PostgreSQL configuration spring.datasource.url=jdbc:postgresql://localhost:5432/powerauth spring.datasource.username=powerauth spring.datasource.password= -spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.properties.hibernate.connection.CharSet=UTF-8 spring.jpa.properties.hibernate.connection.characterEncoding=UTF-8 spring.jpa.properties.hibernate.connection.useUnicode=true @@ -36,3 +34,6 @@ banner.application.name=${spring.application.name} banner.application.version=@project.version@ logging.config=${POWERAUTH_TEST_SERVER_LOGGING:} + +# Monitoring +management.tracing.sampling.probability=1.0 diff --git a/powerauth-webflow-tests/pom.xml b/powerauth-webflow-tests/pom.xml index 912a5f48..bd85cbdb 100644 --- a/powerauth-webflow-tests/pom.xml +++ b/powerauth-webflow-tests/pom.xml @@ -8,7 +8,7 @@ com.wultra powerauth-backend-tests-parent - 1.6.0 + 1.7.0 com.wultra