From 9d2d1dd0aef3e9bb6252d4d976d1139eaac40cc4 Mon Sep 17 00:00:00 2001 From: Peter Sorotokin Date: Mon, 18 Nov 2024 12:30:35 -0800 Subject: [PATCH] Bar codes for credential offers on our openid4vci server page. (#780) Signed-off-by: Peter Sorotokin --- server-openid4vci/build.gradle.kts | 1 + .../identity/server/openid4vci/QrServlet.kt | 32 +++++++++++++++++++ server/src/main/webapp/WEB-INF/web.xml | 12 +++++++ .../webapp/openid4vci/display_metadata.js | 22 +++++++------ 4 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 server-openid4vci/src/main/java/com/android/identity/server/openid4vci/QrServlet.kt 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