diff --git a/server-openid4vci/build.gradle.kts b/server-openid4vci/build.gradle.kts
index b0e928e6f..92c193e75 100644
--- a/server-openid4vci/build.gradle.kts
+++ b/server-openid4vci/build.gradle.kts
@@ -35,6 +35,7 @@ dependencies {
implementation(libs.kotlinx.io.bytestring)
implementation(libs.bouncy.castle.bcprov)
implementation(libs.nimbus.oauth2.oidc.sdk)
+ implementation(libs.zxing.core)
testImplementation(libs.junit)
}
diff --git a/server-openid4vci/src/main/java/com/android/identity/server/openid4vci/QrServlet.kt b/server-openid4vci/src/main/java/com/android/identity/server/openid4vci/QrServlet.kt
new file mode 100644
index 000000000..2bc149103
--- /dev/null
+++ b/server-openid4vci/src/main/java/com/android/identity/server/openid4vci/QrServlet.kt
@@ -0,0 +1,32 @@
+package com.android.identity.server.openid4vci
+
+import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
+import com.google.zxing.qrcode.encoder.Encoder
+import jakarta.servlet.http.HttpServletRequest
+import jakarta.servlet.http.HttpServletResponse
+import java.awt.image.BufferedImage
+import java.awt.image.DataBufferByte
+import javax.imageio.ImageIO
+
+/**
+ * Servlet that encodes text string (passed as `q` parameter) into a QR code.
+ */
+class QrServlet : BaseServlet() {
+ override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
+ val str = req.getParameter("q") ?: throw IllegalStateException("q parameter required")
+ val qr = Encoder.encode(str, ErrorCorrectionLevel.L, null)
+ val matrix = qr.matrix
+ val width = matrix.width
+ val height = matrix.height
+ val image = BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY)
+ val pixels = (image.raster.dataBuffer as DataBufferByte).data
+ var index = 0
+ for (y in 0 until height) {
+ for (x in 0 until width) {
+ pixels[index++] = if (matrix[x, y].toInt() == 0) -1 else 0
+ }
+ }
+ resp.contentType = "image/png"
+ ImageIO.write(image, "png", resp.outputStream)
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/webapp/WEB-INF/web.xml b/server/src/main/webapp/WEB-INF/web.xml
index 9be74baeb..b1464a0cf 100644
--- a/server/src/main/webapp/WEB-INF/web.xml
+++ b/server/src/main/webapp/WEB-INF/web.xml
@@ -367,6 +367,18 @@
/openid4vci/credential_request
+
+ QrServlet
+ QrServlet
+ com.android.identity.server.openid4vci.QrServlet
+ 10
+
+
+
+ QrServlet
+ /openid4vci/qr
+
+
WellKnownOpenidCredentialIssuanceServlet
WellKnownOpenidCredentialIssuanceServlet
diff --git a/server/src/main/webapp/openid4vci/display_metadata.js b/server/src/main/webapp/openid4vci/display_metadata.js
index 27cd3040a..31c967878 100644
--- a/server/src/main/webapp/openid4vci/display_metadata.js
+++ b/server/src/main/webapp/openid4vci/display_metadata.js
@@ -5,28 +5,28 @@ async function displayMetadata() {
let issuance = await (await fetch(".well-known/openid-credential-issuer")).json();
let hi = document.createElement("img");
hi.setAttribute("src", issuance.display[0].logo?.uri);
- hi.setAttribute("style", "width:20%; float: right");
+ hi.setAttribute("style", "width:20%;max-width:180px;float: right");
body.appendChild(hi);
let h1 = document.createElement("h1");
h1.textContent = issuance.display[0].name;
body.appendChild(h1);
- let list = document.createElement("ul");
let configs = issuance.credential_configurations_supported;
let h2 = document.createElement("h2");
h2.textContent = "Credentials available from this server";
body.appendChild(h2);
for (let configId in configs) {
let config = configs[configId];
- let item = document.createElement("li");
- list.appendChild(item);
+ let item = document.createElement("div");
+ item.setAttribute("style", "border-top: 2px solid black; margin: 1em")
+ body.appendChild(item);
+ let h3 = document.createElement("h3");
+ item.appendChild(h3);
let a = document.createElement("a");
item.appendChild(a);
- let h3 = document.createElement("h3");
- a.appendChild(h3);
h3.textContent = config.display[0].name;
let img = document.createElement("img");
img.setAttribute("src", config.display[0].logo?.uri);
- img.setAttribute("style", "width:80%;margin-bottom:2em");
+ img.setAttribute("style", "width:80%;margin:1em;max-width:500px");
a.appendChild(img);
let url = location.href.substring(0, location.href.lastIndexOf("/"));
let offer = {
@@ -36,7 +36,11 @@ async function displayMetadata() {
authorization_code:{}
}
};
- a.href = "openid-credential-offer://?credential_offer=" + encodeURIComponent(JSON.stringify(offer))
+ let href = "openid-credential-offer://?credential_offer=" + encodeURIComponent(JSON.stringify(offer));
+ a.href = href;
+ let qr = document.createElement("img");
+ qr.setAttribute("src", "qr?q=" + encodeURIComponent(href));
+ qr.setAttribute("style", "width:70%;margin:1em;margin-left:2em;image-rendering: pixelated;max-width:450px");
+ item.appendChild(qr);
}
- body.appendChild(list)
}
\ No newline at end of file