diff --git a/.github/workflows/build-powerauth-fido2-tests-docker-image.yml b/.github/workflows/build-powerauth-fido2-tests-docker-image.yml
new file mode 100644
index 00000000..ff0bb57e
--- /dev/null
+++ b/.github/workflows/build-powerauth-fido2-tests-docker-image.yml
@@ -0,0 +1,73 @@
+
+name: Build and push docker image of Powerauth Fido2 Demo to Docker registry
+
+on:
+ workflow_dispatch:
+ pull_request:
+ branches:
+ - 'develop'
+ - 'main'
+ - 'releases/*'
+ paths:
+ - 'powerauth-fido2-tests/**'
+ push:
+ branches:
+ - 'develop'
+ paths:
+ - 'powerauth-fido2-tests/**'
+
+jobs:
+ build:
+ runs-on: 'ubuntu-latest'
+ environment: docker-publish
+ env:
+ # these are global secrets - for readonly access to artifactory
+ INTERNAL_USERNAME: ${{ secrets.JFROG_USERNAME }}
+ INTERNAL_PASSWORD: ${{ secrets.JFROG_PASSWORD }}
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ sparse-checkout: powerauth-fido2-tests
+ - uses: actions/setup-java@v4
+ with:
+ java-version: '21'
+ distribution: 'temurin'
+ server-id: jfrog-central
+ server-username: INTERNAL_USERNAME
+ server-password: INTERNAL_PASSWORD
+ - name: Get version
+ run: |
+ cd powerauth-fido2-tests
+ REVISION=`mvn help:evaluate -Dexpression=project.version -q -DforceStdout`
+ echo "REVISION=$REVISION" >> $GITHUB_ENV
+ - name: Build war
+ run: |
+ cd powerauth-fido2-tests
+ mvn -U -DuseInternalRepo=true --no-transfer-progress package
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+ with:
+ install: true
+ - name: Log in to Azure registry
+ if: ${{ github.actor != 'dependabot[bot]' && (github.event_name == 'workflow_dispatch' || github.event_name == 'push') }}
+ uses: docker/login-action@v3
+ with:
+ registry: https://powerauth.azurecr.io/
+ username: ${{ vars.ACR_USERNAME }}
+ password: ${{ secrets.ACR_PASSWORD }}
+ - name: Build and push container image to Azure registry
+ uses: docker/build-push-action@v6
+ with:
+ push: ${{ github.actor != 'dependabot[bot]' && (github.event_name == 'workflow_dispatch' || github.event_name == 'push') }}
+ platforms: linux/amd64,linux/arm64
+ tags: powerauth.azurecr.io/powerauth-fido2-tests:${{ github.sha }}
+ file: ./powerauth-fido2-tests/docker-powerauth-fido2-tests/Dockerfile
+ context: ./powerauth-fido2-tests
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+ - if: ${{ github.actor != 'dependabot[bot]' && (github.event_name == 'workflow_dispatch' || github.event_name == 'push') }}
+ run: |
+ echo '### 🚀 Published images' >> $GITHUB_STEP_SUMMARY
+ echo 'powerauth.azurecr.io/powerauth-fido2-tests:${{ github.sha }}' >> $GITHUB_STEP_SUMMARY
diff --git a/powerauth-fido2-tests/deploy/conf/fido2-demo.xml b/powerauth-fido2-tests/deploy/conf/fido2-demo.xml
new file mode 100644
index 00000000..023f7d87
--- /dev/null
+++ b/powerauth-fido2-tests/deploy/conf/fido2-demo.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/powerauth-fido2-tests/deploy/conf/logback/fido2-demo-logback.xml b/powerauth-fido2-tests/deploy/conf/logback/fido2-demo-logback.xml
new file mode 100644
index 00000000..15edf478
--- /dev/null
+++ b/powerauth-fido2-tests/deploy/conf/logback/fido2-demo-logback.xml
@@ -0,0 +1,13 @@
+
+
+
+
+ true
+ {"appname":"fido2-demo"}
+
+
+
+
+
+
+
diff --git a/powerauth-fido2-tests/deploy/docker-entrypoint.sh b/powerauth-fido2-tests/deploy/docker-entrypoint.sh
new file mode 100755
index 00000000..ef9d318d
--- /dev/null
+++ b/powerauth-fido2-tests/deploy/docker-entrypoint.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+nginx
+
+catalina.sh run
diff --git a/powerauth-fido2-tests/deploy/nginx/html/favicon.ico b/powerauth-fido2-tests/deploy/nginx/html/favicon.ico
new file mode 100644
index 00000000..48f40e65
Binary files /dev/null and b/powerauth-fido2-tests/deploy/nginx/html/favicon.ico differ
diff --git a/powerauth-fido2-tests/deploy/nginx/nginx.conf b/powerauth-fido2-tests/deploy/nginx/nginx.conf
new file mode 100644
index 00000000..c568584a
--- /dev/null
+++ b/powerauth-fido2-tests/deploy/nginx/nginx.conf
@@ -0,0 +1,134 @@
+worker_processes 2;
+pid /tmp/nginx.pid;
+
+events {
+
+ use epoll;
+ accept_mutex on;
+ worker_connections 512;
+
+}
+
+http {
+
+ client_body_temp_path /tmp/client_temp;
+ proxy_temp_path /tmp/proxy_temp_path;
+ fastcgi_temp_path /tmp/fastcgi_temp;
+ uwsgi_temp_path /tmp/uwsgi_temp;
+ scgi_temp_path /tmp/scgi_temp;
+
+ server_tokens off;
+
+ tcp_nodelay on;
+ tcp_nopush on;
+
+ # Logging
+
+ # Excludes logging for requests with HTTP status codes 2xx (Success) and 3xx (Redirection)
+ map $status $loggable {
+ ~^[23] 0;
+ default 1;
+ }
+
+ log_format custom_format 'measure#nginx.service=$request_time content_type=$content_type '
+ 'content_length=$content_length request_length=$request_length request_time=$request_time '
+ 'status=$status';
+ access_log '/dev/stdout' custom_format if=$loggable;
+ error_log '/dev/stderr';
+
+ include mime.types;
+ default_type application/json;
+ sendfile on;
+
+ # Defines a timeout for reading client request body, period between two successive read operations (default 60s)
+ client_body_timeout 10s;
+
+ # Allows FastCGI server responses with codes greater than or equal to 300 to be passed to a client
+ fastcgi_intercept_errors on;
+
+ # Defines a timeout for establishing a connection with a proxied server (default 60s)
+ proxy_connect_timeout 10s;
+
+ # Defines a timeout for reading a response from the proxied server (default 60s)
+ proxy_read_timeout 29s;
+
+ # Server name must be without underscores
+ upstream demo {
+ server localhost:8080 fail_timeout=0;
+ }
+
+ server {
+ listen 8000;
+ #listen [::]:80 default_server ipv6only=on;
+
+ # error pages rewriting
+ location @401_json {
+ default_type application/json;
+ return 200 '{"status":"ERROR","responseObject":{"code":"HTTP_401","message":"Unauthorized"}}';
+ }
+
+ location @403_json {
+ default_type application/json;
+ return 200 '{"status":"ERROR","responseObject":{"code":"HTTP_403","message":"Forbidden"}}';
+ }
+
+ error_page 404 @404_json;
+
+ location @404_json {
+ default_type application/json;
+ return 404 '{"status":"ERROR","responseObject":{"code":"HTTP_404","message":"Not Found"}}';
+ }
+
+ error_page 500 502 503 504 @500_json;
+ location @500_json {
+ default_type application/json;
+ return 200 '{"status":"ERROR","responseObject":{"code":"ERROR_GENERIC","message":"Unknown Error"}}';
+ }
+
+ # Sets a $real_scheme variable whose value is the scheme passed by the load
+ # balancer in X-Forwarded-Proto, or to X-AppService-Proto in case of Azure
+ # cloud deployment.
+ set $real_scheme "http";
+ if ($http_x_forwarded_proto = "https") { # Generic proxy
+ set $real_scheme "https";
+ }
+ if ($http_x_appservice_proto = "https") { # Azure proxy
+ set $real_scheme "https";
+ }
+
+ add_header Permissions-Policy "publickey-credentials-get=*; publickey-credentials-create=*";
+
+ # global proxy configuration
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Forwarded-Host $http_host;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $real_scheme;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_cookie_flags ~ secure samesite=none;
+
+ proxy_intercept_errors on;
+ proxy_pass_request_headers on;
+
+ location ~ ^(/health|/fido2-demo) {
+ error_page 401 @401_json;
+
+ error_page 403 @403_json;
+
+ rewrite ^/health$ /fido2-demo/actuator/health break;
+ rewrite ^/fido2-demo$ $real_scheme://$http_host/fido2-demo/ permanent;
+
+ proxy_pass http://demo;
+ }
+
+ location = /favicon.ico {
+ alias /etc/nginx/html/favicon.ico;
+ }
+
+ location = / {
+ return 301 $real_scheme://$http_host/fido2-demo/;
+ }
+
+
+ }
+
+}
diff --git a/powerauth-fido2-tests/docker-powerauth-fido2-tests/Dockerfile b/powerauth-fido2-tests/docker-powerauth-fido2-tests/Dockerfile
new file mode 100644
index 00000000..32ad2524
--- /dev/null
+++ b/powerauth-fido2-tests/docker-powerauth-fido2-tests/Dockerfile
@@ -0,0 +1,167 @@
+FROM ibm-semeru-runtimes:open-21.0.5_11-jre
+
+# Prepare environment variables
+ENV JAVA_HOME=/opt/java/openjdk \
+ NGINX_VERSION=1.25.4 \
+ NJS_VERSION=0.8.3 \
+ PKG_RELEASE=1~jammy \
+ TOMCAT_HOME=/usr/local/tomcat \
+ TOMCAT_MAJOR=10 \
+ TOMCAT_VERSION=10.1.25 \
+ TOMCAT_ARCHIVE_SHA512=d7498e23e54425d728ed3481579dccc4fe3d720a4b6d491ce9a04f9d19647b60a398b76dbfec63a32f7ee98195b97231d34b6f850283f38a1acb9908d3015565 \
+ LOGBACK_CONF=/opt/logback/conf \
+ TZ=UTC
+
+ENV PATH=$PATH:$TOMCAT_HOME/bin
+
+# Init
+RUN apt-get -y update \
+ && apt-get -y upgrade \
+ && apt-get -y install bash curl wget postgresql-client cron rsyslog
+
+# 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 \
+ && [ "$TOMCAT_ARCHIVE_SHA512 /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
+
+# Install nginx - source: https://github.com/nginxinc/docker-nginx/blob/master/mainline/debian/Dockerfile
+RUN set -x \
+ && apt-get update \
+ && apt-get install --no-install-recommends --no-install-suggests -y gnupg1 ca-certificates \
+ && \
+ NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62; \
+ found=''; \
+ for server in \
+ hkp://keyserver.ubuntu.com:80 \
+ pgp.mit.edu \
+ ; do \
+ echo "Fetching GPG key $NGINX_GPGKEY from $server"; \
+ apt-key adv --keyserver "$server" --keyserver-options timeout=10 --recv-keys "$NGINX_GPGKEY" && found=yes && break; \
+ done; \
+ test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \
+ apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \
+ && dpkgArch="$(dpkg --print-architecture)" \
+ && nginxPackages=" \
+ nginx=${NGINX_VERSION}-${PKG_RELEASE} \
+ nginx-module-xslt=${NGINX_VERSION}-${PKG_RELEASE} \
+ nginx-module-geoip=${NGINX_VERSION}-${PKG_RELEASE} \
+ nginx-module-image-filter=${NGINX_VERSION}-${PKG_RELEASE} \
+ nginx-module-njs=${NGINX_VERSION}+${NJS_VERSION}-${PKG_RELEASE} \
+ " \
+ && case "$dpkgArch" in \
+ amd64|arm64) \
+# arches officialy built by upstream
+ echo "deb https://nginx.org/packages/mainline/ubuntu jammy nginx" >> /etc/apt/sources.list.d/nginx.list \
+ && apt-get update \
+ ;; \
+ *) \
+# we're on an architecture upstream doesn't officially build for
+# let's build binaries from the published source packages
+ echo "deb-src https://nginx.org/packages/mainline/ubuntu jammy nginx" >> /etc/apt/sources.list.d/nginx.list \
+ \
+# new directory for storing sources and .deb files
+ && tempDir="$(mktemp -d)" \
+ && chmod 777 "$tempDir" \
+# (777 to ensure APT's "_apt" user can access it too)
+ \
+# save list of currently-installed packages so build dependencies can be cleanly removed later
+ && savedAptMark="$(apt-mark showmanual)" \
+ \
+# build .deb files from upstream's source packages (which are verified by apt-get)
+ && apt-get update \
+ && apt-get build-dep -y $nginxPackages \
+ && ( \
+ cd "$tempDir" \
+ && DEB_BUILD_OPTIONS="nocheck parallel=$(nproc)" \
+ apt-get source --compile $nginxPackages \
+ ) \
+# we don't remove APT lists here because they get re-downloaded and removed later
+ \
+# reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies
+# (which is done after we install the built packages so we don't have to redownload any overlapping dependencies)
+ && apt-mark showmanual | xargs apt-mark auto > /dev/null \
+ && { [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark; } \
+ \
+# create a temporary local APT repo to install from (so that dependency resolution can be handled by APT, as it should be)
+ && ls -lAFh "$tempDir" \
+ && ( cd "$tempDir" && dpkg-scanpackages . > Packages ) \
+ && grep '^Package: ' "$tempDir/Packages" \
+ && echo "deb [ trusted=yes ] file://$tempDir ./" > /etc/apt/sources.list.d/temp.list \
+# work around the following APT issue by using "Acquire::GzipIndexes=false" (overriding "/etc/apt/apt.conf.d/docker-gzip-indexes")
+# Could not open file /var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages - open (13: Permission denied)
+# ...
+# E: Failed to fetch store:/var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages Could not open file /var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages - open (13: Permission denied)
+ && apt-get -o Acquire::GzipIndexes=false update \
+ ;; \
+ esac \
+ \
+ && apt-get install --no-install-recommends --no-install-suggests -y \
+ $nginxPackages \
+ gettext-base \
+ curl \
+ && apt-get remove --purge --auto-remove -y && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/nginx.list \
+ \
+# if we have leftovers from building, let's purge them (including extra, unnecessary build deps)
+ && if [ -n "$tempDir" ]; then \
+ apt-get purge -y --auto-remove \
+ && rm -rf "$tempDir" /etc/apt/sources.list.d/temp.list; \
+ fi \
+# forward request and error logs to docker log collector
+ && ln -sf /dev/stdout /var/log/nginx/access.log \
+ && ln -sf /dev/stderr /var/log/nginx/error.log \
+# create a docker-entrypoint.d directory
+ && mkdir /docker-entrypoint.d
+
+# Copy nginx configuration and files
+COPY deploy/nginx/ /etc/nginx/
+
+# Clear root context
+RUN rm -rf $TOMCAT_HOME/webapps/*
+
+# Optimize tomcat startup
+# allow parallel start of app modules, auto-detection based on number of available cores
+RUN sed -i -e 's///g' $TOMCAT_HOME/conf/context.xml \
+ && echo 'org.apache.catalina.startup.TldConfig.jarsToSkip=*.jar' >> $TOMCAT_HOME/conf/catalina.properties \
+# remove context configuration scanning
+ && sed -i -e 's/tomcat.util.scan.StandardJarScanFilter.jarsToSkip=\\/tomcat.util.scan.StandardJarScanFilter.jarsToSkip=*,\\/g' $TOMCAT_HOME/conf/catalina.properties
+
+# Add valve for proxy with SSL termination
+RUN sed -i -e 's/<\/Host>/<\/Host>/' $TOMCAT_HOME/conf/server.xml
+
+# Avoid having JSESSIONID in URI
+RUN sed -i -e 's||\n COOKIE|' $TOMCAT_HOME/conf/web.xml
+
+# Deploy and run applications
+COPY deploy/conf/fido2-demo.xml \
+ $TOMCAT_HOME/conf/Catalina/localhost/
+
+COPY target/powerauth-fido2-tests*.war $TOMCAT_HOME/webapps/fido2-demo.war
+
+COPY deploy/conf/logback/* $LOGBACK_CONF/
+
+RUN set -x \
+# Uninstall packages which are no longer needed and clean apt caches
+ && apt-get -y remove wget curl gettext-base \
+ && apt-get -y purge --auto-remove \
+ && rm -rf /tmp/* /var/cache/apt/*
+
+
+# Docker configuration
+EXPOSE 8000
+STOPSIGNAL SIGQUIT
+
+# Add PowerAuth User
+RUN groupadd -r powerauth \
+ && useradd -r -g powerauth -s /sbin/nologin powerauth \
+ && chown -R powerauth:powerauth $TOMCAT_HOME \
+ && chown -R powerauth:powerauth /opt/apache-tomcat-$TOMCAT_VERSION
+USER powerauth
+
+# Define entry point
+COPY deploy/docker-entrypoint.sh /
+ENTRYPOINT ["/docker-entrypoint.sh"]