Skip to content

Commit 8cb29cd

Browse files
NFC-99 Add web-eid-1.1 token support
Signed-off-by: Sander Kondratjev <[email protected]>
1 parent 762501b commit 8cb29cd

24 files changed

+1438
-135
lines changed

README.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,116 @@ When using standard [ASP.NET cookie authentication](https://docs.microsoft.com/e
298298
}
299299
```
300300

301+
- Similarly, the `MobileAuthInitController` generates a challenge nonce and returns the mobile deep-link for starting the Web eID Mobile authentication flow, and the `MobileAuthLoginController` handles the mobile login request by validating the returned authentication token and creating the authentication cookie.
302+
```cs
303+
using System;
304+
using System.Text;
305+
using Microsoft.AspNetCore.Mvc;
306+
using Microsoft.Extensions.Options;
307+
using System.Text.Json;
308+
using System.Text.Json.Serialization;
309+
using Options;
310+
using Security.Challenge;
311+
312+
[ApiController]
313+
[Route("auth/mobile")]
314+
public class MobileAuthInitController(
315+
IChallengeNonceGenerator nonceGenerator,
316+
IOptions<WebEidMobileOptions> mobileOptions
317+
) : ControllerBase
318+
{
319+
private const string WebEidMobileAuthPath = "auth";
320+
private const string MobileLoginPath = "/auth/mobile/login";
321+
322+
[HttpPost("init")]
323+
public IActionResult Init()
324+
{
325+
var challenge = nonceGenerator.GenerateAndStoreNonce(TimeSpan.FromMinutes(5));
326+
var challengeBase64 = challenge.Base64EncodedNonce;
327+
328+
var loginUri = $"{Request.Scheme}://{Request.Host}{MobileLoginPath}";
329+
330+
var payload = new AuthPayload
331+
{
332+
Challenge = challengeBase64,
333+
LoginUri = loginUri,
334+
GetSigningCertificate = mobileOptions.Value.RequestSigningCert ? true : null
335+
};
336+
337+
var json = JsonSerializer.Serialize(payload);
338+
var encodedPayload = Convert.ToBase64String(Encoding.UTF8.GetBytes(json));
339+
340+
var authUri = BuildAuthUri(encodedPayload);
341+
342+
return Ok(new AuthUri
343+
{
344+
AuthUriValue = authUri
345+
});
346+
}
347+
```
348+
349+
```cs
350+
using Microsoft.AspNetCore.Mvc;
351+
using System.Text.Json;
352+
using Dto;
353+
using Security.Challenge;
354+
using Security.Validator;
355+
using System.Security.Claims;
356+
using System.Threading.Tasks;
357+
using Microsoft.AspNetCore.Authentication;
358+
using Microsoft.AspNetCore.Authentication.Cookies;
359+
using Security.Util;
360+
361+
[ApiController]
362+
[Route("auth/mobile")]
363+
public class MobileAuthLoginController(
364+
IAuthTokenValidator authTokenValidator,
365+
IChallengeNonceStore challengeNonceStore
366+
) : ControllerBase
367+
{
368+
[HttpPost("login")]
369+
public async Task<IActionResult> MobileLogin([FromBody] AuthenticateRequestDto dto)
370+
{
371+
if (dto?.AuthToken == null)
372+
{
373+
return BadRequest(new { error = "Missing auth_token" });
374+
}
375+
376+
var parsedToken = dto.AuthToken;
377+
var certificate = await authTokenValidator.Validate(
378+
parsedToken,
379+
challengeNonceStore.GetAndRemove().Base64EncodedNonce);
380+
381+
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
382+
383+
identity.AddClaim(new Claim(ClaimTypes.GivenName, certificate.GetSubjectGivenName()));
384+
identity.AddClaim(new Claim(ClaimTypes.Surname, certificate.GetSubjectSurname()));
385+
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, certificate.GetSubjectIdCode()));
386+
identity.AddClaim(new Claim(ClaimTypes.Name, certificate.GetSubjectCn()));
387+
388+
if (!string.IsNullOrEmpty(parsedToken.UnverifiedSigningCertificate))
389+
{
390+
identity.AddClaim(new Claim("signingCertificate", parsedToken.UnverifiedSigningCertificate));
391+
}
392+
393+
if (parsedToken.SupportedSignatureAlgorithms != null)
394+
{
395+
identity.AddClaim(new Claim(
396+
"supportedSignatureAlgorithms",
397+
JsonSerializer.Serialize(parsedToken.SupportedSignatureAlgorithms)));
398+
}
399+
400+
await HttpContext.SignInAsync(
401+
CookieAuthenticationDefaults.AuthenticationScheme,
402+
new ClaimsPrincipal(identity),
403+
new AuthenticationProperties { IsPersistent = false });
404+
405+
return Ok(new { redirect = "/welcome" });
406+
}
407+
}
408+
```
409+
410+
301411
# Table of contents
302412

303413
* [Introduction](#introduction)

example/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ The `src\WebEid.AspNetCore.Example` directory contains the ASP.NET application s
130130
- `DigiDoc`: contains the C# binding files of the `libdigidocpp` library; these files must be copied from the `libdigidocpp` installation directory `\include\digidocpp_csharp`,
131131
- `Pages`: Razor pages,
132132
- `Services`: Web eID signing service implementation that uses `libdigidocpp`.
133+
- `Options`: strongly-typed configuration classes for mobile Web eID settings such as `BaseRequestUri` and `RequestSigningCert` (when set to false, initiates a separate signing-certificate flow to demo requesting the certificate without prior authentication, as the signing certificate normally comes from the authentication flow).
133134

134135
## More information
135136

src/WebEid.Security.Tests/TestUtils/AbstractTestWithValidator.cs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ public abstract class AbstractTestWithValidator
3434
"\"appVersion\":\"https://web-eid.eu/web-eid-app/releases/2.0.0+0\"," +
3535
"\"signature\":\"tbMTrZD4CKUj6atjNCHZruIeyPFAEJk2htziQ1t08BSTyA5wKKqmNmzsJ7562hWQ6+tJd6nlidHGE5jVVJRKmPtNv3f9gbT2b7RXcD4t5Pjn8eUCBCA4IX99Af32Z5ln\"," +
3636
"\"format\":\"web-eid:1\"}";
37+
38+
protected const string ValidV11AuthTokenStr = "{\"algorithm\":\"ES384\"," +
39+
"\"unverifiedCertificate\":\"MIIEBDCCA2WgAwIBAgIQY5OGshxoPMFg+Wfc0gFEaTAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTIxMDcyMjEyNDMwOFoXDTI2MDcwOTIxNTk1OVowfzELMAkGA1UEBhMCRUUxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEQMA4GA1UEBAwHSsOVRU9SRzEWMBQGA1UEKgwNSkFBSy1LUklTVEpBTjEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQmwEKsJTjaMHSaZj19hb9EJaJlwbKc5VFzmlGMFSJVk4dDy+eUxa5KOA7tWXqzcmhh5SYdv+MxcaQKlKWLMa36pfgv20FpEDb03GCtLqjLTRZ7649PugAQ5EmAqIic29CjggHDMIIBvzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBHBgNVHSAEQDA+MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAIBgYEAI96AQIwHwYDVR0RBBgwFoEUMzgwMDEwODU3MThAZWVzdGkuZWUwHQYDVR0OBBYEFPlp/ceABC52itoqppEmbf71TJz6MGEGCCsGAQUFBwEDBFUwUzBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBjAAwgYgCQgDCAgybz0u3W+tGI+AX+PiI5CrE9ptEHO5eezR1Jo4j7iGaO0i39xTGUB+NSC7P6AQbyE/ywqJjA1a62jTLcS9GHAJCARxN4NO4eVdWU3zVohCXm8WN3DWA7XUcn9TZiLGQ29P4xfQZOXJi/z4PNRRsR4plvSNB3dfyBvZn31HhC7my8woi\"," +
40+
"\"unverifiedSigningCertificate\":\"MIID6zCCA02gAwIBAgIQT7j6zk6pmVRcyspLo5SqejAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTE5MDUwMjEwNDUzMVoXDTI5MDUwMjEwNDUzMVowfzELMAkGA1UEBhMCRUUxFjAUBgNVBCoMDUpBQUstS1JJU1RKQU4xEDAOBgNVBAQMB0rDlUVPUkcxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASkwENR8GmCpEs6OshDWDfIiKvGuyNMOD2rjIQW321AnZD3oIsqD0svBMNEJJj9Dlvq/47TYDObIa12KAU5IuOBfJs2lrFdSXZjaM+a5TWT3O2JTM36YDH2GcMe/eisepejggGrMIIBpzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDBIBgNVHSAEQTA/MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAJBgcEAIvsQAECMB0GA1UdDgQWBBTVX3s48Spy/Es2TcXgkRvwUn2YcjCBigYIKwYBBQUHAQMEfjB8MAgGBgQAjkYBATAIBgYEAI5GAQQwEwYGBACORgEGMAkGBwQAjkYBBgEwUQYGBACORgEFMEcwRRY/aHR0cHM6Ly9zay5lZS9lbi9yZXBvc2l0b3J5L2NvbmRpdGlvbnMtZm9yLXVzZS1vZi1jZXJ0aWZpY2F0ZXMvEwJFTjAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBiwAwgYcCQgGBr+Jbo1GeqgWdIwgMo7SA29AP38JxNm2HWq2Qb+kIHpusAK574Co1K5D4+Mk7/ITTuXQaET5WphHoN7tdAciTaQJBAn0zBigYyVPYSTO68HM6hmlwTwi/KlJDdXW/2NsMjSqofFFJXpGvpxk2CTqSRCjcavxLPnkasTbNROYSJcmM8Xc=\"," +
41+
"\"supportedSignatureAlgorithms\":[{\"cryptoAlgorithm\":\"RSA\",\"hashFunction\":\"SHA-256\",\"paddingScheme\":\"PKCS1.5\"}]," +
42+
"\"appVersion\":\"https://web-eid.eu/web-eid-mobile-app/releases/v1.0.0\"," +
43+
"\"signature\":\"0Ov7ME6pTY1K2GXMj8Wxov/o2fGIMEds8OMY5dKdkB0nrqQX7fG1E5mnsbvyHpMDecMUH6Yg+p1HXdgB/lLqOcFZjt/OVXPjAAApC5d1YgRYATDcxsR1zqQwiNcHdmWn\"," +
44+
"\"format\":\"web-eid:1.1\"}";
45+
3746
public const string ValidChallengeNonce = "12345678123456781234567812345678912356789123";
3847

3948
private DateTimeProvider dateTimeProvider;
@@ -44,15 +53,22 @@ public abstract class AbstractTestWithValidator
4453
[SetUp]
4554
protected void SetUp()
4655
{
47-
this.Validator = AuthTokenValidators.GetAuthTokenValidator();
48-
this.ValidAuthToken = this.Validator.Parse(ValidAuthTokenStr);
49-
this.dateTimeProvider = DateTimeProvider.OverrideUtcNow(new DateTime(2021, 3, 1));
56+
Validator = AuthTokenValidators.GetAuthTokenValidator();
57+
ValidAuthToken = Validator.Parse(ValidAuthTokenStr);
58+
dateTimeProvider = DateTimeProvider.OverrideUtcNow(new DateTime(2021, 8, 1));
5059
}
5160

5261
[TearDown]
53-
public void TearDown() => this.dateTimeProvider?.Dispose();
62+
public void TearDown() => dateTimeProvider?.Dispose();
5463

5564
protected WebEidAuthToken ReplaceTokenField(string token, string field, string value) =>
56-
this.Validator.Parse(token.Replace(field, value));
65+
Validator.Parse(token.Replace(field, value));
66+
67+
protected string RemoveJsonField(string json, string fieldName)
68+
{
69+
var node = Newtonsoft.Json.Linq.JObject.Parse(json);
70+
node.Remove(fieldName);
71+
return node.ToString(Newtonsoft.Json.Formatting.None);
72+
}
5773
}
5874
}

src/WebEid.Security.Tests/Validator/AuthTokenAlgorithmTest.cs

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,33 +22,77 @@
2222
namespace WebEid.Security.Tests.Validator
2323
{
2424
using NUnit.Framework;
25-
using WebEid.Security.Exceptions;
26-
using WebEid.Security.Tests.TestUtils;
25+
using Exceptions;
26+
using TestUtils;
2727

2828
public class AuthTokenAlgorithmTest : AbstractTestWithValidator
2929
{
3030
[Test]
3131
public void WhenAlgorithmNoneThenValidationFailsAsync()
3232
{
33-
var authToken = this.ReplaceTokenField(ValidAuthTokenStr, "ES384", "NONE");
34-
Assert.ThrowsAsync<AuthTokenParseException>(() => this.Validator.Validate(authToken, ValidChallengeNonce))
33+
var authToken = ReplaceTokenField(ValidAuthTokenStr, "ES384", "NONE");
34+
Assert.ThrowsAsync<AuthTokenParseException>(() => Validator.Validate(authToken, ValidChallengeNonce))
3535
.WithMessage("Unsupported signature algorithm");
3636
}
3737

3838
[Test]
3939
public void WhenAlgorithmEmptyThenParsingFailsAsync()
4040
{
41-
var authToken = this.ReplaceTokenField(ValidAuthTokenStr, "ES384", "");
42-
Assert.ThrowsAsync<AuthTokenParseException>(() => this.Validator.Validate(authToken, ValidChallengeNonce))
41+
var authToken = ReplaceTokenField(ValidAuthTokenStr, "ES384", "");
42+
Assert.ThrowsAsync<AuthTokenParseException>(() => Validator.Validate(authToken, ValidChallengeNonce))
4343
.WithMessage("'algorithm' is null or empty");
4444
}
4545

4646
[Test]
4747
public void WhenAlgorithmInvalidThenParsingFailsAsync()
4848
{
49-
var authToken = this.ReplaceTokenField(ValidAuthTokenStr, "ES384", "\u0000\t\ninvalid");
50-
Assert.ThrowsAsync<AuthTokenParseException>(() => this.Validator.Validate(authToken, ValidChallengeNonce))
49+
var authToken = ReplaceTokenField(ValidAuthTokenStr, "ES384", "\u0000\t\ninvalid");
50+
Assert.ThrowsAsync<AuthTokenParseException>(() => Validator.Validate(authToken, ValidChallengeNonce))
5151
.WithMessage("Unsupported signature algorithm");
5252
}
53+
54+
[Test]
55+
public void WhenV11TokenMissingSupportedAlgorithmsThenValidationFailsAsync()
56+
{
57+
var tokenJson = RemoveJsonField(ValidV11AuthTokenStr, "supportedSignatureAlgorithms");
58+
var token = Validator.Parse(tokenJson);
59+
60+
var ex = Assert.ThrowsAsync<AuthTokenParseException>(() =>
61+
Validator.Validate(token, ValidChallengeNonce));
62+
63+
Assert.That(ex.Message, Does.Contain("'supportedSignatureAlgorithms' field is missing"));
64+
}
65+
66+
[Test]
67+
public void WhenV11TokenHasInvalidCryptoAlgorithmThenValidationFailsAsync()
68+
{
69+
var token = ReplaceTokenField(ValidV11AuthTokenStr, "\"cryptoAlgorithm\":\"RSA\"", "\"cryptoAlgorithm\":\"INVALID\"");
70+
Assert.ThrowsAsync<AuthTokenParseException>(() => Validator.Validate(token, ValidChallengeNonce))
71+
.WithMessage("Unsupported signature algorithm");
72+
}
73+
74+
[Test]
75+
public void WhenV11TokenHasInvalidHashFunctionThenValidationFailsAsync()
76+
{
77+
var token = ReplaceTokenField( ValidV11AuthTokenStr, "\"hashFunction\":\"SHA-256\"", "\"hashFunction\":\"NOT_A_HASH\"");
78+
Assert.ThrowsAsync<AuthTokenParseException>(() => Validator.Validate(token, ValidChallengeNonce))
79+
.WithMessage("Unsupported signature algorithm");
80+
}
81+
82+
[Test]
83+
public void WhenV11TokenHasInvalidPaddingSchemeThenValidationFailsAsync()
84+
{
85+
var token = ReplaceTokenField( ValidV11AuthTokenStr, "\"paddingScheme\":\"PKCS1.5\"", "\"paddingScheme\":\"BAD_PADDING\"");
86+
Assert.ThrowsAsync<AuthTokenParseException>(() => Validator.Validate(token, ValidChallengeNonce))
87+
.WithMessage("Unsupported signature algorithm");
88+
}
89+
90+
[Test]
91+
public void WhenV11TokenHasEmptySupportedAlgorithmsThenValidationFailsAsync()
92+
{
93+
var token = ReplaceTokenField( ValidV11AuthTokenStr, "\"supportedSignatureAlgorithms\":[{\"cryptoAlgorithm\":\"RSA\",\"hashFunction\":\"SHA-256\",\"paddingScheme\":\"PKCS1.5\"}]", "\"supportedSignatureAlgorithms\":[]");
94+
Assert.ThrowsAsync<AuthTokenParseException>(() => Validator.Validate(token, ValidChallengeNonce))
95+
.WithMessage("'supportedSignatureAlgorithms' field is missing");
96+
}
5397
}
5498
}

0 commit comments

Comments
 (0)