diff --git a/README.md b/README.md index a58e265..488ac32 100644 --- a/README.md +++ b/README.md @@ -196,8 +196,9 @@ A REST endpoint that issues challenge nonces is required for authentication. The In the following example, we are using the [ASP.NET Web APIs RESTful Web Services framework](https://dotnet.microsoft.com/apps/aspnet/apis) to implement the endpoint, see also full implementation [here](https://github.com/web-eid/web-eid-authtoken-validation-dotnet/blob/main/example/src/WebEid.AspNetCore.Example/Controllers/Api/AuthController.cs). ```cs +using System; using Microsoft.AspNetCore.Mvc; -using WebEid.Security.Nonce; +using WebEid.Security.Challenge; [ApiController] [Route("auth")] @@ -253,79 +254,126 @@ When using standard [ASP.NET cookie authentication](https://docs.microsoft.com/e using System; using System.Collections.Generic; using System.Security.Claims; - using System.Text.Json.Serialization; + using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Mvc; + using WebEid.AspNetCore.Example.Dto; + using WebEid.Security.AuthToken; + using WebEid.Security.Challenge; + using WebEid.Security.Exceptions; using WebEid.Security.Util; using WebEid.Security.Validator; [Route("[controller]")] [ApiController] - public class AuthController : ControllerBase + public class AuthController(IAuthTokenValidator authTokenValidator, IChallengeNonceStore challengeNonceStore) : BaseController { - private readonly IAuthTokenValidator authTokenValidator; + private readonly IAuthTokenValidator authTokenValidator = authTokenValidator; + private readonly IChallengeNonceStore challengeNonceStore = challengeNonceStore; - public AuthController(IAuthTokenValidator authTokenValidator, IChallengeNonceStore challengeNonceStore) + [HttpPost("login")] + public async Task Login([FromBody] AuthenticateRequestDto dto) { - this.authTokenValidator = authTokenValidator; - this.challengeNonceStore = challengeNonceStore; + if (dto?.AuthToken is null) + { + return BadRequest(new { error = "Missing auth_token" }); + } + + try + { + await SignInUser(dto.AuthToken); + return Ok(); + } + catch (Exception ex) when (ex is ChallengeNonceNotFoundException or ChallengeNonceExpiredException) + { + return Unauthorized(new { error = "challenge_nonce_not_found_or_expired" }); + } + catch (AuthTokenException) + { + return Unauthorized(new { error = "authentication_failed" }); + } } - [HttpPost] - [ValidateAntiForgeryToken] - [Route("login")] - public async Task Login([FromBody] AuthenticateRequestDto authToken) + [HttpPost("logout")] + public async Task Logout() { - var certificate = await this.authTokenValidator.Validate(authToken.AuthToken, this.challengeNonceStore.GetAndRemove().Base64EncodedNonce); - var claims = new List + if (HasActiveSession()) { - new Claim(ClaimTypes.GivenName, certificate.GetSubjectGivenName()), - new Claim(ClaimTypes.Surname, certificate.GetSubjectSurname()), - new Claim(ClaimTypes.NameIdentifier, certificate.GetSubjectIdCode()) - }; + RemoveUserContainerFile(); + } + + await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + } - var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); + private async Task SignInUser(WebEidAuthToken authToken) + { + var certificate = await authTokenValidator.Validate(authToken, challengeNonceStore.GetAndRemove().Base64EncodedNonce); + var claims = new List(); + + 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 authProperties = new AuthenticationProperties + var signingCertificate = authToken.UnverifiedSigningCertificates != null && + authToken.UnverifiedSigningCertificates.Count > 0 + ? authToken.UnverifiedSigningCertificates[0] + : null; + + if (signingCertificate != null && !string.IsNullOrEmpty(signingCertificate.Certificate)) { - AllowRefresh = true - }; + claims.Add(new Claim("signingCertificate", signingCertificate.Certificate)); + } + + if (signingCertificate?.SupportedSignatureAlgorithms != null) + { + claims.Add(new Claim( + "supportedSignatureAlgorithms", + JsonSerializer.Serialize(signingCertificate.SupportedSignatureAlgorithms))); + } + + var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); await HttpContext.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, - new ClaimsPrincipal(claimsIdentity), - authProperties); - } + new ClaimsPrincipal(identity), + new AuthenticationProperties { AllowRefresh = true }); - [HttpGet] - [ValidateAntiForgeryToken] - [Route("logout")] - public async Task Logout() + SetUniqueIdInSession(); + } + + private static void AddNewClaimIfCertificateHasData(List claims, string claimType, Func dataGetter) { - await HttpContext.SignOutAsync( - CookieAuthenticationDefaults.AuthenticationScheme); + var claimData = dataGetter(); + if (!string.IsNullOrEmpty(claimData)) + { + claims.Add(new Claim(claimType, claimData)); + } } } ``` - -- 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 +- Similarly, the `MobileAuthInitController` generates a challenge nonce and returns the authentication request link (OS-verified App Link / Universal Link) for starting the Web eID mobile authentication flow, and the `AuthController` 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; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.Options; + using WebEid.AspNetCore.Example.Options; + using WebEid.AspNetCore.Example.Services; + using WebEid.Security.Challenge; [ApiController] [Route("auth/mobile")] public class MobileAuthInitController( IChallengeNonceGenerator nonceGenerator, - IOptions mobileOptions + IOptions mobileOptions, + MobileRequestUriBuilder uriBuilder, + IConfiguration configuration ) : ControllerBase { private const string WebEidMobileAuthPath = "auth"; @@ -337,7 +385,7 @@ When using standard [ASP.NET cookie authentication](https://docs.microsoft.com/e var challenge = nonceGenerator.GenerateAndStoreNonce(TimeSpan.FromMinutes(5)); var challengeBase64 = challenge.Base64EncodedNonce; - var loginUri = $"{Request.Scheme}://{Request.Host}{MobileLoginPath}"; + var loginUri = $"{configuration["OriginUrl"]}{MobileLoginPath}"; var payload = new AuthPayload { @@ -349,80 +397,43 @@ When using standard [ASP.NET cookie authentication](https://docs.microsoft.com/e var json = JsonSerializer.Serialize(payload); var encodedPayload = Convert.ToBase64String(Encoding.UTF8.GetBytes(json)); - var authUri = BuildAuthUri(encodedPayload); + var authUri = uriBuilder.Build(WebEidMobileAuthPath, 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 MobileLogin([FromBody] AuthenticateRequestDto dto) + private sealed record AuthPayload { - if (dto?.AuthToken == null) - { - return BadRequest(new { error = "Missing auth_token" }); - } - - var parsedToken = dto.AuthToken; - var certificate = await authTokenValidator.Validate( - parsedToken, - challengeNonceStore.GetAndRemove().Base64EncodedNonce); + [JsonInclude] + [JsonPropertyName("challenge")] + public required string Challenge { get; init; } - var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme); + [JsonInclude] + [JsonPropertyName("loginUri")] + public required string LoginUri { get; init; } - 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 }); + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("getSigningCertificate")] + public bool? GetSigningCertificate { get; init; } + } - return Ok(new { redirect = "/welcome" }); + private sealed record AuthUri + { + [JsonInclude] + [JsonPropertyName("authUri")] + public required string AuthUriValue { get; init; } } } - ``` - + ``` # Table of contents +* [Quickstart](#quickstart) * [Introduction](#introduction) +* [Authentication token format](#authentication-token-format) * [Authentication token validation](#authentication-token-validation) * [Basic usage](#basic-usage) * [Extended configuration](#extended-configuration) @@ -432,19 +443,115 @@ When using standard [ASP.NET cookie authentication](https://docs.microsoft.com/e * [Stateful and stateless authentication](#stateful-and-stateless-authentication) * [Challenge nonce generation](#challenge-nonce-generation) * [Basic usage](#basic-usage-1) +* [Authentication token format versions](#authentication-token-format-versions) # Introduction -The Web eID authentication token validation library for .NET contains the implementation of the Web eID authentication token validation process in its entirety to ensure that the authentication token sent by the Web eID browser extension contains valid, consistent data that has not been modified by a third party. It also implements secure challenge nonce generation as required by the Web eID authentication protocol. It is easy to configure and integrate into your authentication service. +The Web eID authentication token validation library for .NET contains the implementation of the Web eID authentication token validation process in its entirety to ensure that the authentication token sent by the Web eID browser extension or mobile application contains valid, consistent data that has not been modified by a third party. It also implements secure challenge nonce generation as required by the Web eID authentication protocol. It is easy to configure and integrate into your authentication service. The authentication protocol, validation requirements, authentication token format and nonce usage are described in more detail in the [Web eID system architecture document](https://github.com/web-eid/web-eid-system-architecture-doc#authentication-1). +# Authentication token format + +In the following, + +- **origin** is defined as the website origin, the URL serving the web application, +- **challenge nonce** (or challenge) is defined as a cryptographic nonce, a large random number that can be used only once, with at least 256 bits of entropy. + +The Web eID authentication token (format **`web-eid:1.0`**) is a JSON data structure that looks like the following example: + +```json +{ + "unverifiedCertificate": "MIIFozCCA4ugAwIBAgIQHFpdK-zCQsFW4...", + "algorithm": "RS256", + "signature": "HBjNXIaUskXbfhzYQHvwjKDUWfNu4yxXZha...", + "format": "web-eid:1.0", + "appVersion": "https://web-eid.eu/web-eid-app/releases/v2.0.0" +} +``` + +It contains the following fields: + +- `unverifiedCertificate`: the base64-encoded DER-encoded authentication certificate of the eID user; the public key contained in this certificate should be used to verify the signature; the certificate cannot be trusted as it is received from client side and the client can submit a malicious certificate; to establish trust, it must be verified that the certificate is signed by a trusted certificate authority, + +- `algorithm`: the signature algorithm used to produce the signature; the allowed values are the algorithms specified in [JWA RFC](https://www.ietf.org/rfc/rfc7518.html) sections 3.3, 3.4 and 3.5: + + ``` + "ES256", "ES384", "ES512", // ECDSA + "PS256", "PS384", "PS512", // RSASSA-PSS + "RS256", "RS384", "RS512" // RSASSA-PKCS1-v1_5 + ``` + +- `signature`: the base64-encoded signature of the token (see the description below), + +- `format`: the type identifier and version of the token format separated by a colon character '`:`', `web-eid:1.0` or `web-eid:1.1` as of now; the version number consists of the major and minor number separated by a dot, major version changes are incompatible with previous versions, minor version changes are backwards-compatible within the given major version, + +- `appVersion`: the URL identifying the name and version of the application that issued the token; informative purpose, can be used to identify the affected application in case of faulty tokens. + +The value that is signed by the user’s authentication private key and included in the `signature` field is `hash(origin)+hash(challenge)`. The hash function is used before concatenation to ensure field separation as the hash of a value is guaranteed to have a fixed length. Otherwise the origin `example.com` with challenge nonce `.eu1234` and another origin `example.com.eu` with challenge nonce `1234` would result in the same value after concatenation. The hash function `hash` is the same hash function that is used in the signature algorithm, for example SHA256 in case of RS256. + +The Web eID authentication token (format **`web-eid:1.1`**) is a JSON data structure that looks like the following example: + +```json +{ + "unverifiedCertificate": "MIIFozCCA4ugAwIBAgIQHFpdK-zCQsFW4...", + "algorithm": "RS256", + "signature": "HBjNXIaUskXbfhzYQHvwjKDUWfNu4yxXZha...", + "unverifiedSigningCertificates": [ + { + "certificate": "MIIFikACB3ugAwASAgIHHFrtdZ-zeQsas1...", + "supportedSignatureAlgorithms": [ + { + "cryptoAlgorithm": "ECC", + "hashFunction": "SHA-384", + "paddingScheme": "NONE" + } + ] + } + ], + "format": "web-eid:1.1", + "appVersion": "https://web-eid.eu/web-eid-app/releases/v2.0.0" +} +``` +It contains the following fields: + +- `unverifiedSigningCertificates`: an array of objects containing signing certificate information. + +Each object inside `unverifiedSigningCertificates` contains: + +- `certificate`: base64-encoded DER-encoded signing certificate, + +- `supportedSignatureAlgorithms`: list of supported algorithms in the following format: + + - `cryptoAlgorithm`: the cryptographic algorithm used for the key, + + - `hashFunction`: the hashing algorithm used, + + - `paddingScheme`: the padding scheme used (if applicable). + + +Allowed values are: + + cryptoAlgorithm: "ECC", "RSA" + + hashFunction: + "SHA-224", "SHA-256", "SHA-384", "SHA-512", + "SHA3-224", "SHA3-256", "SHA3-384", "SHA3-512" + + paddingScheme: "NONE", "PKCS1.5", "PSS" + # Authentication token validation -The authentication token validation process consists of two stages: +The authentication token validation process consists of the following stages: - First, **user certificate validation**: the validator parses the token and extracts the user certificate from the *unverifiedCertificate* field. Then it checks the certificate expiration, purpose and policies. Next it checks that the certificate is signed by a trusted CA and checks the certificate status with OCSP. - Second, **token signature validation**: the validator validates that the token signature was created using the provided user certificate by reconstructing the signed data `hash(origin)+hash(challenge)` and using the public key from the certificate to verify the signature in the `signature` field. If the signature verification succeeds, then the origin and challenge nonce have been implicitly and correctly verified without the need to implement any additional security checks. +- Additional validation for **Web eID authentication tokens (format v1.1)**: the token must contain the `unverifiedSigningCertificates` field with at least one signing certificate entry. Each entry's `supportedSignatureAlgorithms` are validated against the set of allowed cryptographic algorithms, hash functions, and padding schemes. For each signing certificate, the following checks are performed: + - The subject must match the subject of the authentication certificate, ensuring both certificates belong to the same user. + - The issuing authority must match that of the authentication certificate, verified via the Authority Key Identifier (AKI) extension. + - The certificate must be within its validity period. + - The certificate must contain the non-repudiation key usage bit required for digital signatures. + - The certificate chain must validate against the configured trusted certificate authorities. The website backend must lookup the challenge nonce from its local store using an identifier specific to the browser session, to guarantee that the authentication token was received from the same browser to which the corresponding challenge nonce was issued. The website backend must guarantee that the challenge nonce lifetime is limited and that its expiration is checked, and that it can be used only once by removing it from the store during validation. @@ -532,7 +639,7 @@ A common alternative to stateful authentication is stateless authentication with # Challenge nonce generation -The authentication protocol requires support for generating challenge nonces, large random numbers that can be used only once, and storing them for later use during token validation. The validation library uses the *System.Security.Cryptography.RandomNumberGenerator* API as the secure random source and provides *WebEid.Security.Cache.ICache* interface for storing issued challenge nonces. +The authentication protocol requires support for generating challenge nonces, large random numbers that can be used only once, and storing them for later use during token validation. The validation library uses the *System.Security.Cryptography.RandomNumberGenerator* API as the secure random source and the `IChallengeNonceStore` interface for storing issued challenge nonces. The authentication protocol requires a REST endpoint that issues challenge nonces as described in section *[6. Add a REST endpoint for issuing challenge nonces](#6-add-a-rest-endpoint-for-issuing-challenge-nonces)*. @@ -562,6 +669,19 @@ To format the library code, run: dotnet format src/WebEid.Security.sln --no-restore ``` +# Authentication token format versions + +The Web eID authentication protocol defines two token formats currently supported by this library: + +- **Format v1.0** – Used in desktop Web eID authentication flows with traditional smart card readers. + +- **Format v1.1** – An extended authentication token format that allows signing certificate information to be included in the authentication response. + - `unverifiedSigningCertificates` – an array of signing certificate entries. Each entry contains: + - `certificate` – a base64-encoded DER-encoded signing certificate; + - `supportedSignatureAlgorithms` – a list of supported signature algorithms associated with that certificate; + +Both token formats follow the same validation principles, differing only in the structure of embedded certificates and the additional verification steps required for v1.1. + ## Feedback For technical support or to report issues, please submit a [support ticket](https://github.com/web-eid/web-eid-authtoken-validation-dotnet/issues) or contact our support team at [help@ria.ee](mailto:help@ria.ee). diff --git a/example/README.md b/example/README.md index 8b8a3f6..6ae12e8 100644 --- a/example/README.md +++ b/example/README.md @@ -6,13 +6,6 @@ This project is an example ASP.NET web application that shows how to implement s More information about the Web eID project is available on the project [website](https://web-eid.eu/). -The ASP.NET web application makes use of the following technologies: - -- ASP.NET MVC, -- the Web eID authentication token validation library [_web-eid-authtoken-validation-dotnet_](https://github.com/web-eid/web-eid-authtoken-validation-dotnet), -- the Web eID JavaScript library [_web-eid.js_](https://github.com/web-eid/web-eid.js), -- the digital signing library [_libdigidocpp_](https://github.com/open-eid/libdigidocpp/tree/master/examples/DigiDocCSharp). - ## Quickstart Complete the steps below to run the example application in order to test authentication and digital signing with Web eID. @@ -160,7 +153,32 @@ This will activate the `https` profile in the `launchSettings.json` and launch t When the application has started, open your preferred web browser on the address defined in `launchSettings.json` on the `applicationUrl` field at `https` profile and follow instructions on the front page. By default the address is https://localhost:44391. -## Overview of the source code +## Table of contents + +* [Quickstart](#quickstart) +* [Setup for Development](#setup-for-development) +* [Overview of the project](#overview-of-the-project) + + [Overview of the source code](#overview-of-the-source-code) + + [Requesting the signing certificate in a separate step](#requesting-the-signing-certificate-in-a-separate-step) +* [More information](#more-information) + + [Frequently asked questions](#frequently-asked-questions) + - [Why do I get the `System.ApplicationException: Failed to verify OCSP Responder certificate` error during signing?](#why-do-i-get-the-systemapplicationexception-failed-to-verify-ocsp-responder-certificate-error-during-signing) +* [Building and running example web application with Docker on Ubuntu Linux](#building-and-running-example-web-application-with-docker-on-ubuntu-linux) + + [Prerequisites](#prerequisites) + + [Building the application](#building-the-application) + + [Building the Docker image](#building-the-docker-image) +* [Running the Docker container with HTTPS support](#running-the-docker-container-with-https-support) + +## Overview of the project + +The ASP.NET web application makes use of the following technologies: + +- ASP.NET MVC, +- the Web eID authentication token validation library [_web-eid-authtoken-validation-dotnet_](https://github.com/web-eid/web-eid-authtoken-validation-dotnet), +- the Web eID JavaScript library [_web-eid.js_](https://github.com/web-eid/web-eid.js), +- the digital signing library [_libdigidocpp_](https://github.com/open-eid/libdigidocpp/tree/master/examples/DigiDocCSharp). + +### Overview of the source code The `src\WebEid.AspNetCore.Example` directory contains the ASP.NET application source code and resources. The subdirectories therein have the following purpose: - `wwwroot`: web server static content, including CSS and JavaScript files, @@ -170,10 +188,24 @@ The `src\WebEid.AspNetCore.Example` directory contains the ASP.NET application s - logging in, - 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`, +- `Dto`: data transfer objects used by the Web API endpoints, - `Pages`: Razor pages, -- `Services`: Web eID signing service implementation that uses `libdigidocpp`. +- `Services`: helper services for cleaning up signing containers and for building the mobile authentication and signing request URIs, +- `Signing`: Web eID signing service implementation that uses `libdigidocpp`, + - `SigningService`: prepares signing containers and finalizes signatures, + - `MobileSigningService`: orchestrates the mobile signing flow (builds mobile signing requests/responses) and supports requesting the signing certificate in a separate step when enabled by configuration, - `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). +### Requesting the signing certificate in a separate step + +In some deployments, the signing certificate is not reused from the authentication flow. Instead, it is retrieved directly from the user’s ID-card during the signing process itself. + +This approach is useful when the signing process is performed without a prior authentication step. For example, in a mobile flow, the user may start signing directly without authenticating beforehand. In such cases, the signing certificate must be requested separately from the user’s ID-card before the signature can be created. + +When this mode is enabled in the configuration, the backend issues a separate request for the signing certificate using the `MobileSigningService`. The service communicates with the client to obtain the certificate before the signing container is prepared, ensuring that the correct certificate chain is available for the signature. + +This behavior is controlled by the `RequestSigningCert` flag in the `appsettings.json` configuration files (`appsettings.json`, `appsettings.Development.json`). When the flag is set to **false**, the application explicitly requests the signing certificate during the signing process, demonstrating the separate signing certificate retrieval flow. When set to **true**, the signing uses the signing certificate that was already obtained during authentication, and no additional request is made. + ## More information See the [Web eID Java example application documentation](https://github.com/web-eid/web-eid-spring-boot-example) for more information, including answers to questions not answered below.