Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,116 @@ When using standard [ASP.NET cookie authentication](https://docs.microsoft.com/e
}
```

- 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.
```cs
using System;
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using System.Text.Json;
using System.Text.Json.Serialization;
using Options;
using Security.Challenge;

[ApiController]
[Route("auth/mobile")]
public class MobileAuthInitController(
IChallengeNonceGenerator nonceGenerator,
IOptions<WebEidMobileOptions> mobileOptions
) : ControllerBase
{
private const string WebEidMobileAuthPath = "auth";
private const string MobileLoginPath = "/auth/mobile/login";

[HttpPost("init")]
public IActionResult Init()
{
var challenge = nonceGenerator.GenerateAndStoreNonce(TimeSpan.FromMinutes(5));
var challengeBase64 = challenge.Base64EncodedNonce;

var loginUri = $"{Request.Scheme}://{Request.Host}{MobileLoginPath}";

var payload = new AuthPayload
{
Challenge = challengeBase64,
LoginUri = loginUri,
GetSigningCertificate = mobileOptions.Value.RequestSigningCert ? true : null
};

var json = JsonSerializer.Serialize(payload);
var encodedPayload = Convert.ToBase64String(Encoding.UTF8.GetBytes(json));

var authUri = BuildAuthUri(encodedPayload);

return Ok(new AuthUri
{
AuthUriValue = authUri
});
}
```

```cs
using Microsoft.AspNetCore.Mvc;
using System.Text.Json;
using Dto;
using Security.Challenge;
using Security.Validator;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Security.Util;

[ApiController]
[Route("auth/mobile")]
public class MobileAuthLoginController(
IAuthTokenValidator authTokenValidator,
IChallengeNonceStore challengeNonceStore
) : ControllerBase
{
[HttpPost("login")]
public async Task<IActionResult> MobileLogin([FromBody] AuthenticateRequestDto dto)
{
if (dto?.AuthToken == null)
{
return BadRequest(new { error = "Missing auth_token" });
}

var parsedToken = dto.AuthToken;
var certificate = await authTokenValidator.Validate(
parsedToken,
challengeNonceStore.GetAndRemove().Base64EncodedNonce);

var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);

identity.AddClaim(new Claim(ClaimTypes.GivenName, certificate.GetSubjectGivenName()));
identity.AddClaim(new Claim(ClaimTypes.Surname, certificate.GetSubjectSurname()));
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, certificate.GetSubjectIdCode()));
identity.AddClaim(new Claim(ClaimTypes.Name, certificate.GetSubjectCn()));

if (!string.IsNullOrEmpty(parsedToken.UnverifiedSigningCertificate))
{
identity.AddClaim(new Claim("signingCertificate", parsedToken.UnverifiedSigningCertificate));
}

if (parsedToken.SupportedSignatureAlgorithms != null)
{
identity.AddClaim(new Claim(
"supportedSignatureAlgorithms",
JsonSerializer.Serialize(parsedToken.SupportedSignatureAlgorithms)));
}

await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(identity),
new AuthenticationProperties { IsPersistent = false });

return Ok(new { redirect = "/welcome" });
}
}
```


# Table of contents

* [Introduction](#introduction)
Expand Down
3 changes: 2 additions & 1 deletion example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ The `src\WebEid.AspNetCore.Example` directory contains the ASP.NET application s
- digital signing,
- `DigiDoc`: contains the C# binding files of the `libdigidocpp` library; these files must be copied from the `libdigidocpp` installation directory `\include\digidocpp_csharp`,
- `Pages`: Razor pages,
- `Signing`: Web eID signing service implementation that uses `libdigidocpp`.
- `Services`: Web eID signing service implementation that uses `libdigidocpp`.
- `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).

## More information

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@ namespace WebEid.AspNetCore.Example.Controllers.Api
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using Security.Util;
using Security.Validator;
using System.Collections.Generic;
using System.Security.Claims;
using System.Text.Json;
using System.Threading.Tasks;
using Security.Challenge;
using WebEid.AspNetCore.Example.Dto;
using WebEid.Security.AuthToken;
using System;

[Route("[controller]")]
Expand All @@ -44,44 +47,68 @@ public AuthController(IAuthTokenValidator authTokenValidator, IChallengeNonceSto
this.challengeNonceStore = challengeNonceStore;
}

[HttpPost]
[Route("login")]
public async Task Login([FromBody] AuthenticateRequestDto authToken)
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] AuthenticateRequestDto dto)
{
var certificate = await this.authTokenValidator.Validate(authToken.AuthToken, this.challengeNonceStore.GetAndRemove().Base64EncodedNonce);
try
{
await SignInUser(dto?.AuthToken);
return Ok();
}
catch (ArgumentNullException)
{
return BadRequest(new { error = "Missing auth_token" });
}
}

List<Claim> claims = new();
[HttpPost("logout")]
public async Task Logout()
{
RemoveUserContainerFile();
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}

private async Task SignInUser(WebEidAuthToken authToken)
{
if (authToken == null)
{
throw new ArgumentNullException(nameof(authToken), "authToken must not be null");
}

var certificate = await authTokenValidator.Validate(authToken, challengeNonceStore.GetAndRemove().Base64EncodedNonce);
var claims = new List<Claim>();

AddNewClaimIfCertificateHasData(claims, ClaimTypes.GivenName, certificate.GetSubjectGivenName);
AddNewClaimIfCertificateHasData(claims, ClaimTypes.Surname, certificate.GetSubjectSurname);
AddNewClaimIfCertificateHasData(claims, ClaimTypes.NameIdentifier, certificate.GetSubjectIdCode);
AddNewClaimIfCertificateHasData(claims, ClaimTypes.Name, certificate.GetSubjectCn);

var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
if (!string.IsNullOrEmpty(authToken.UnverifiedSigningCertificate))
{
claims.Add(new Claim(
"signingCertificate",
authToken.UnverifiedSigningCertificate));
}

var authProperties = new AuthenticationProperties
if (authToken.SupportedSignatureAlgorithms != null)
{
AllowRefresh = true
};
claims.Add(new Claim(
"supportedSignatureAlgorithms",
JsonSerializer.Serialize(authToken.SupportedSignatureAlgorithms)));
}

var identity = new ClaimsIdentity(claims,CookieAuthenticationDefaults.AuthenticationScheme);

await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
new ClaimsPrincipal(identity),
new AuthenticationProperties { AllowRefresh = true });

// Assign a unique ID within the session to enable the use of a unique temporary container name across successive requests.
// A unique temporary container name is required to facilitate simultaneous signing from multiple browsers.
SetUniqueIdInSession();
}

[HttpGet]
[Route("logout")]
public async Task Logout()
{
RemoveUserContainerFile();
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}

private static void AddNewClaimIfCertificateHasData(List<Claim> claims, string claimType, Func<string> dataGetter)
{
var claimData = dataGetter();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright (c) 2025-2025 Estonian Information System Authority
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

namespace WebEid.AspNetCore.Example.Controllers.Api
{
using System;
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using System.Text.Json;
using System.Text.Json.Serialization;
using Options;
using Security.Challenge;

[ApiController]
[Route("auth/mobile")]
public class MobileAuthInitController(
IChallengeNonceGenerator nonceGenerator,
IOptions<WebEidMobileOptions> mobileOptions
) : ControllerBase
{
private const string WebEidMobileAuthPath = "auth";
private const string MobileLoginPath = "/auth/mobile/login";

[HttpPost("init")]
public IActionResult Init()
{
var challenge = nonceGenerator.GenerateAndStoreNonce(TimeSpan.FromMinutes(5));
var challengeBase64 = challenge.Base64EncodedNonce;

var loginUri = $"{Request.Scheme}://{Request.Host}{MobileLoginPath}";

var payload = new AuthPayload
{
Challenge = challengeBase64,
LoginUri = loginUri,
GetSigningCertificate = mobileOptions.Value.RequestSigningCert ? true : null
};

var json = JsonSerializer.Serialize(payload);
var encodedPayload = Convert.ToBase64String(Encoding.UTF8.GetBytes(json));

var authUri = BuildAuthUri(encodedPayload);

return Ok(new AuthUri
{
AuthUriValue = authUri
});
}

private string BuildAuthUri(string encodedPayload)
{
var baseUri = mobileOptions.Value.BaseRequestUri;

return baseUri.StartsWith("http", StringComparison.OrdinalIgnoreCase)
? $"{baseUri.TrimEnd('/')}/{WebEidMobileAuthPath}#{encodedPayload}"
: $"{baseUri}{WebEidMobileAuthPath}#{encodedPayload}";
}

private sealed record AuthPayload
{
[JsonInclude]
[JsonPropertyName("challenge")]
public required string Challenge { get; init; }

[JsonInclude]
[JsonPropertyName("login_uri")]
public required string LoginUri { get; init; }

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("get_signing_certificate")]
public bool? GetSigningCertificate { get; init; }
}

private sealed record AuthUri
{
[JsonInclude]
[JsonPropertyName("auth_uri")]
public required string AuthUriValue { get; init; }
}
}
}
Loading
Loading