Skip to content

Commit b011dcb

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

20 files changed

+925
-109
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright (c) 2021-2024 Estonian Information System Authority
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy of
4+
// this software and associated documentation files (the "Software"), to deal in
5+
// the Software without restriction, including without limitation the rights to
6+
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7+
// the Software, and to permit persons to whom the Software is furnished to do so,
8+
// subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in all
11+
// copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15+
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16+
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17+
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18+
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19+
20+
namespace WebEid.AspNetCore.Example.Controllers.Api
21+
{
22+
using System;
23+
using System.Text;
24+
using Microsoft.AspNetCore.Mvc;
25+
using Microsoft.Extensions.Options;
26+
using System.Text.Json;
27+
using System.Text.Json.Serialization;
28+
using Options;
29+
using Security.Challenge;
30+
31+
[ApiController]
32+
[Route("auth/mobile")]
33+
public class MobileAuthInitController(
34+
IChallengeNonceGenerator nonceGenerator,
35+
IOptions<WebEidMobileOptions> mobileOptions
36+
) : ControllerBase
37+
{
38+
private const string WebEidMobileAuthPath = "auth";
39+
private const string MobileLoginPath = "/auth/mobile/login";
40+
41+
[HttpPost("init")]
42+
public IActionResult Init()
43+
{
44+
var challenge = nonceGenerator.GenerateAndStoreNonce(TimeSpan.FromMinutes(5));
45+
var challengeBase64 = challenge.Base64EncodedNonce;
46+
47+
var loginUri = $"{Request.Scheme}://{Request.Host}{MobileLoginPath}";
48+
49+
var payload = new AuthPayload
50+
{
51+
Challenge = challengeBase64,
52+
LoginUri = loginUri,
53+
GetSigningCertificate = mobileOptions.Value.RequestSigningCert ? true : null
54+
};
55+
56+
var json = JsonSerializer.Serialize(payload);
57+
var encodedPayload = Convert.ToBase64String(Encoding.UTF8.GetBytes(json));
58+
59+
var authUri = BuildAuthUri(encodedPayload);
60+
61+
return Ok(new AuthUri
62+
{
63+
AuthUriValue = authUri
64+
});
65+
}
66+
67+
private string BuildAuthUri(string encodedPayload)
68+
{
69+
var baseUri = mobileOptions.Value.BaseRequestUri;
70+
71+
return baseUri.StartsWith("http", StringComparison.OrdinalIgnoreCase)
72+
? $"{baseUri.TrimEnd('/')}/{WebEidMobileAuthPath}#{encodedPayload}"
73+
: $"{baseUri}{WebEidMobileAuthPath}#{encodedPayload}";
74+
}
75+
76+
private sealed record AuthPayload
77+
{
78+
[JsonInclude]
79+
[JsonPropertyName("challenge")]
80+
public required string Challenge { get; init; }
81+
82+
[JsonInclude]
83+
[JsonPropertyName("login_uri")]
84+
public required string LoginUri { get; init; }
85+
86+
[JsonInclude]
87+
[JsonPropertyName("get_signing_certificate")]
88+
public bool? GetSigningCertificate { get; init; }
89+
}
90+
91+
private sealed record AuthUri
92+
{
93+
[JsonInclude]
94+
[JsonPropertyName("auth_uri")]
95+
public required string AuthUriValue { get; init; }
96+
}
97+
}
98+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright (c) 2021-2024 Estonian Information System Authority
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy of
4+
// this software and associated documentation files (the "Software"), to deal in
5+
// the Software without restriction, including without limitation the rights to
6+
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7+
// the Software, and to permit persons to whom the Software is furnished to do so,
8+
// subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in all
11+
// copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15+
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16+
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17+
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18+
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19+
20+
namespace WebEid.AspNetCore.Example.Controllers.Api
21+
{
22+
using Microsoft.AspNetCore.Mvc;
23+
using System.Text.Json;
24+
using Dto;
25+
using Security.Challenge;
26+
using Security.Validator;
27+
using System.Security.Claims;
28+
using System.Threading.Tasks;
29+
using Microsoft.AspNetCore.Authentication;
30+
using Microsoft.AspNetCore.Authentication.Cookies;
31+
using Security.Util;
32+
33+
[ApiController]
34+
[Route("auth/mobile")]
35+
public class MobileAuthLoginController(
36+
IAuthTokenValidator authTokenValidator,
37+
IChallengeNonceStore challengeNonceStore
38+
) : ControllerBase
39+
{
40+
[HttpPost("login")]
41+
public async Task<IActionResult> MobileLogin([FromBody] AuthenticateRequestDto dto)
42+
{
43+
if (dto?.AuthToken == null)
44+
{
45+
return BadRequest(new { error = "Missing auth_token" });
46+
}
47+
48+
var parsedToken = dto.AuthToken;
49+
var certificate = await authTokenValidator.Validate(
50+
parsedToken,
51+
challengeNonceStore.GetAndRemove().Base64EncodedNonce);
52+
53+
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
54+
55+
identity.AddClaim(new Claim(ClaimTypes.GivenName, certificate.GetSubjectGivenName()));
56+
identity.AddClaim(new Claim(ClaimTypes.Surname, certificate.GetSubjectSurname()));
57+
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, certificate.GetSubjectIdCode()));
58+
identity.AddClaim(new Claim(ClaimTypes.Name, certificate.GetSubjectCn()));
59+
60+
if (!string.IsNullOrEmpty(parsedToken.UnverifiedSigningCertificate))
61+
{
62+
identity.AddClaim(new Claim("signingCertificate", parsedToken.UnverifiedSigningCertificate));
63+
}
64+
65+
if (parsedToken.SupportedSignatureAlgorithms != null)
66+
{
67+
identity.AddClaim(new Claim(
68+
"supportedSignatureAlgorithms",
69+
JsonSerializer.Serialize(parsedToken.SupportedSignatureAlgorithms)));
70+
}
71+
72+
await HttpContext.SignInAsync(
73+
CookieAuthenticationDefaults.AuthenticationScheme,
74+
new ClaimsPrincipal(identity),
75+
new AuthenticationProperties { IsPersistent = false });
76+
77+
return Ok(new { redirect = "/welcome" });
78+
}
79+
}
80+
}

example/src/WebEid.AspNetCore.Example/Dto/AuthenticateRequestDto.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,13 @@
2424

2525
public class AuthenticateRequestDto
2626
{
27-
[JsonPropertyName("auth-token")]
28-
public WebEidAuthToken AuthToken { get; set; }
27+
// Mobile version uses "auth_token"
28+
[JsonPropertyName("auth_token")] public WebEidAuthToken AuthTokenUnderscore { get; set; }
29+
30+
// Desktop version uses "auth-token"
31+
[JsonPropertyName("auth-token")] public WebEidAuthToken AuthTokenDash { get; set; }
32+
33+
// Unified property for backend logic
34+
[JsonIgnore] public WebEidAuthToken AuthToken => AuthTokenDash ?? AuthTokenUnderscore;
2935
}
3036
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) 2021-2024 Estonian Information System Authority
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy of
4+
// this software and associated documentation files (the "Software"), to deal in
5+
// the Software without restriction, including without limitation the rights to
6+
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7+
// the Software, and to permit persons to whom the Software is furnished to do so,
8+
// subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in all
11+
// copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15+
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16+
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17+
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18+
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19+
20+
namespace WebEid.AspNetCore.Example.Options
21+
{
22+
using System.ComponentModel.DataAnnotations;
23+
24+
public class WebEidMobileOptions
25+
{
26+
[Required]
27+
[RegularExpression("^.*(?:[^/]|://)$", ErrorMessage = "Base URI must not have a trailing slash")]
28+
public string BaseRequestUri { get; set; } = null!;
29+
30+
public bool RequestSigningCert { get; set; }
31+
}
32+
}

0 commit comments

Comments
 (0)