Skip to content

Commit 943bd59

Browse files
NFC-99 Web eID for Mobile signing support for web-eid example
Signed-off-by: Sander Kondratjev <[email protected]>
1 parent b011dcb commit 943bd59

File tree

8 files changed

+478
-90
lines changed

8 files changed

+478
-90
lines changed

example/src/WebEid.AspNetCore.Example/Controllers/Api/SignController.cs

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,30 @@
1717
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
1818
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1919

20-
namespace WebEid.AspNetCore.Example.Controllers.Api
20+
namespace WebEid.AspNetCore.Example.Controllers.Api
2121
{
2222
using System;
2323
using System.Security.Claims;
2424
using System.Threading.Tasks;
2525
using Microsoft.AspNetCore.Mvc;
2626
using Microsoft.Extensions.Logging;
27+
using Dto;
2728
using Services;
28-
using WebEid.AspNetCore.Example.Dto;
29+
using Signing;
2930

3031
[Route("[controller]")]
3132
[ApiController]
3233
public class SignController : BaseController
3334
{
3435
private const string SignedFile = "example-for-signing.asice";
3536
private readonly SigningService signingService;
37+
private readonly MobileSigningService mobileSigningService;
3638
private readonly ILogger logger;
3739

38-
public SignController(SigningService signingService, ILogger logger)
40+
public SignController(SigningService signingService, MobileSigningService mobileSigningService, ILogger logger)
3941
{
4042
this.signingService = signingService;
43+
this.mobileSigningService = mobileSigningService;
4144
this.logger = logger;
4245
}
4346

@@ -56,6 +59,49 @@ public FileDto Sign([FromBody] SignatureDto data)
5659
return new FileDto(SignedFile);
5760
}
5861

62+
[HttpPost("mobile/init")]
63+
public MobileSigningService.MobileInitRequest MobileInit()
64+
{
65+
var identity = (ClaimsIdentity)HttpContext.User.Identity;
66+
var container = GetUserContainerName();
67+
return mobileSigningService.InitCertificateOrSigningRequest(identity, container);
68+
}
69+
70+
[Route("sign/mobile/certificate")]
71+
[HttpGet]
72+
public IActionResult CertificateResponse()
73+
{
74+
return Redirect("/sign/mobile/certificate");
75+
}
76+
77+
[Route("mobile/certificate")]
78+
[HttpPost]
79+
public MobileSigningService.MobileInitRequest CertificatePost([FromBody] CertificateDto certificateDto)
80+
{
81+
var identity = (ClaimsIdentity)HttpContext.User.Identity;
82+
var containerName = GetUserContainerName();
83+
84+
return mobileSigningService.InitSigningRequest(
85+
identity,
86+
certificateDto,
87+
containerName);
88+
}
89+
90+
[Route("sign/mobile/signature")]
91+
[HttpGet]
92+
public IActionResult SignatureResponse()
93+
{
94+
return Redirect("/sign/mobile/signature");
95+
}
96+
97+
[Route("mobile/signature")]
98+
[HttpPost]
99+
public FileDto SignaturePost([FromBody] SignatureDto signatureDto)
100+
{
101+
signingService.SignContainer(signatureDto, GetUserContainerName());
102+
return new FileDto(SignedFile);
103+
}
104+
59105
[Route("download")]
60106
[HttpGet]
61107
public async Task<IActionResult> Download()
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
@page "/sign/mobile/{mode}"
2+
@model WebEid.AspNetCore.Example.Pages.WebEidCallbackModel
3+
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf
4+
5+
@{
6+
var tokens = Xsrf.GetAndStoreTokens(HttpContext);
7+
}
8+
9+
<!DOCTYPE html>
10+
<html lang="en">
11+
<head>
12+
<meta charset="UTF-8"/>
13+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
14+
<meta http-equiv="Cache-Control" content="no-store"/>
15+
<meta id="csrftoken" name="csrftoken" content="@tokens.RequestToken" />
16+
<meta id="csrfheadername" name="csrfheadername" content="@tokens.HeaderName" />
17+
<title>Completing signing…</title>
18+
<link rel="stylesheet" href="/css/bootstrap.min.css"/>
19+
<link rel="stylesheet" href="/css/main.css"/>
20+
</head>
21+
22+
<body class="loading-page">
23+
<div id="spinner-message">
24+
<h2>Completing signing…</h2>
25+
<div class="spinner"></div>
26+
</div>
27+
<div id="error-message" class="alert alert-danger" style="display: none;" role="alert">
28+
<div class="message"></div>
29+
<pre class="details"></pre>
30+
</div>
31+
32+
<p class="text-center p-4" style="display: none;" id="error-actions">
33+
<button id="back-button" class="btn btn-primary">Back</button>
34+
</p>
35+
36+
<script type="module">
37+
import {showErrorMessage, checkHttpError} from '/js/errors.js';
38+
39+
// Using an async IIFE for mobile WebView compatibility:
40+
// top-level await is not supported in some mobile browsers/WebViews.
41+
(async () => {
42+
const fragment = window.location.hash.slice(1);
43+
44+
if (!fragment) {
45+
throw new Error("Missing response payload");
46+
}
47+
48+
let payload;
49+
try {
50+
payload = JSON.parse(atob(fragment));
51+
} catch (e) {
52+
console.error(e)
53+
throw new Error("Failed to parse the response");
54+
}
55+
56+
if (payload.error) {
57+
const error = new Error(payload.message ?? "Signing failed");
58+
error.code = payload.code;
59+
throw error;
60+
}
61+
62+
const path = window.location.pathname;
63+
let endpoint;
64+
if (path === "/sign/mobile/certificate") {
65+
endpoint = "/sign/mobile/certificate";
66+
} else if (path === "/sign/mobile/signature") {
67+
endpoint = "/sign/mobile/signature";
68+
} else {
69+
const error = new Error("Unexpected callback path: " + path);
70+
error.code = "INVALID_CALLBACK_PATH";
71+
throw error;
72+
}
73+
74+
const csrfHeaderName = document.querySelector('#csrfheadername').content;
75+
const csrfToken = document.querySelector('#csrftoken').content;
76+
77+
// The mobile Web eID app always returns this value as a plain string (e.g. "ES256"),
78+
// but .NET backend expects an object { id, name }. Normalize the string form
79+
// into the object form for consistent handling in .NET.
80+
if (payload.signatureAlgorithm && typeof payload.signatureAlgorithm === "string") {
81+
payload.signatureAlgorithm = {
82+
id: payload.signatureAlgorithm,
83+
name: payload.signatureAlgorithm
84+
};
85+
}
86+
87+
const response = await fetch(endpoint, {
88+
method: "POST",
89+
headers: {
90+
"Content-Type": "application/json",
91+
[csrfHeaderName]: csrfToken,
92+
},
93+
credentials: "include",
94+
body: JSON.stringify(payload),
95+
});
96+
await checkHttpError(response);
97+
98+
const result = await response.json();
99+
if (endpoint.endsWith("/certificate")) {
100+
const {request_uri} = result;
101+
window.location.replace(request_uri);
102+
return;
103+
}
104+
105+
window.location.replace(
106+
"/welcome?signed=" + encodeURIComponent(result.name)
107+
);
108+
})().catch((error) => {
109+
console.error(error);
110+
showErrorMessage(error, "Signing failed");
111+
const spinner = document.querySelector("#spinner-message")
112+
spinner.style.display = "none";
113+
const actions = document.getElementById("error-actions");
114+
const goBackButton = document.getElementById("back-button");
115+
actions.style.display = "block";
116+
goBackButton.addEventListener("click", () => {
117+
window.location.replace("/welcome?error=webeid-callback");
118+
});
119+
});
120+
</script>
121+
</body>
122+
</html>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+

2+
namespace WebEid.AspNetCore.Example.Pages
3+
{
4+
using Microsoft.AspNetCore.Mvc.RazorPages;
5+
6+
public class WebEidCallbackModel : PageModel
7+
{
8+
public void OnGet()
9+
{
10+
/* Intentionally left empty. This Razor Page only renders HTML and JavaScript for WebEID callback. */
11+
}
12+
}
13+
}

0 commit comments

Comments
 (0)