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 @@ -298,6 +298,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
1 change: 1 addition & 0 deletions example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ The `src\WebEid.AspNetCore.Example` directory contains the ASP.NET application s
- `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,
- `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
@@ -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; }

[JsonInclude]
[JsonPropertyName("get_signing_certificate")]
public bool? GetSigningCertificate { get; init; }
}

private sealed record AuthUri
{
[JsonInclude]
[JsonPropertyName("auth_uri")]
public required string AuthUriValue { get; init; }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// 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 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" });
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2021-2024 Estonian Information System Authority
// Copyright (c) 2021-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
Expand All @@ -17,27 +17,30 @@
// 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
namespace WebEid.AspNetCore.Example.Controllers.Api
{
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Dto;
using Services;
using WebEid.AspNetCore.Example.Dto;
using Signing;

[Route("[controller]")]
[ApiController]
public class SignController : BaseController
{
private const string SignedFile = "example-for-signing.asice";
private readonly SigningService signingService;
private readonly MobileSigningService mobileSigningService;
private readonly ILogger logger;

public SignController(SigningService signingService, ILogger logger)
public SignController(SigningService signingService, MobileSigningService mobileSigningService, ILogger logger)
{
this.signingService = signingService;
this.mobileSigningService = mobileSigningService;
this.logger = logger;
}

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

[HttpPost("mobile/init")]
public MobileSigningService.MobileInitRequest MobileInit()
{
var identity = (ClaimsIdentity)HttpContext.User.Identity;
var container = GetUserContainerName();
return mobileSigningService.InitCertificateOrSigningRequest(identity, container);
}

[Route("sign/mobile/certificate")]
[HttpGet]
public IActionResult CertificateResponse()
{
return Redirect("/sign/mobile/certificate");
}

[Route("mobile/certificate")]
[HttpPost]
public MobileSigningService.MobileInitRequest CertificatePost([FromBody] CertificateDto certificateDto)
{
var identity = (ClaimsIdentity)HttpContext.User.Identity;
var containerName = GetUserContainerName();

return mobileSigningService.InitSigningRequest(
identity,
certificateDto,
containerName);
}

[Route("sign/mobile/signature")]
[HttpGet]
public IActionResult SignatureResponse()
{
return Redirect("/sign/mobile/signature");
}

[Route("mobile/signature")]
[HttpPost]
public FileDto SignaturePost([FromBody] SignatureDto signatureDto)
{
signingService.SignContainer(signatureDto, GetUserContainerName());
return new FileDto(SignedFile);
}

[Route("download")]
[HttpGet]
public async Task<IActionResult> Download()
Expand Down
Loading
Loading