Imports System.Security.Cryptography.X509Certificates Imports System.Text.RegularExpressions Imports Newtonsoft.Json Namespace Models Public Class CertificateLoginBridgeOptions Public Property PublicUrl As String = String.Empty Public Property ForwardedCertificateHeader As String = "X-ARR-ClientCert" Public Property AdditionalForwardedCertificateHeaders As List(Of String) = New List(Of String)() Public Property AllowedParentOrigins As List(Of String) = New List(Of String)() Public Shared Function LoadFromConfiguration() As CertificateLoginBridgeOptions Dim publicUrl = FirstNonEmpty( ConfigurationManager.AppSettings("CertificateLoginPublicUrl"), ConfigurationManager.AppSettings("UrlCertLogin")) Dim forwardedCertificateHeader = FirstNonEmpty( ConfigurationManager.AppSettings("CertificateForwardedHeader"), ConfigurationManager.AppSettings("CertHeaderName"), "X-ARR-ClientCert") Dim additionalHeadersSetting = FirstNonEmpty( ConfigurationManager.AppSettings("CertificateAdditionalForwardedHeaders"), ConfigurationManager.AppSettings("AdditionalCertHeaders")) Dim allowedOriginsSetting = FirstNonEmpty( ConfigurationManager.AppSettings("CertificateAllowedParentOrigins"), ConfigurationManager.AppSettings("AllowedParentOrigins")) Return New CertificateLoginBridgeOptions With { .PublicUrl = publicUrl, .ForwardedCertificateHeader = forwardedCertificateHeader, .AdditionalForwardedCertificateHeaders = SplitSetting(additionalHeadersSetting), .AllowedParentOrigins = SplitSetting(allowedOriginsSetting) } End Function Private Shared Function SplitSetting(value As String) As List(Of String) If String.IsNullOrWhiteSpace(value) Then Return New List(Of String)() End If Return value.Split(New Char() {";"c, "|"c}, StringSplitOptions.RemoveEmptyEntries). Select(Function(item) item.Trim()). Where(Function(item) item <> String.Empty). ToList() End Function Private Shared Function FirstNonEmpty(ParamArray values() As String) As String For Each value In values If Not String.IsNullOrWhiteSpace(value) Then Return value.Trim() End If Next Return String.Empty End Function End Class Public Class CertificateProxyLoginResponse Public Property Token As String = String.Empty Public Property User As CertificateProxyUser End Class Public Class CertificateProxyUser Public Property NIF As String = String.Empty Public Property NOMBRE As String = String.Empty Public Property APELLIDOS As String = String.Empty Public Property ADMINISTRARPTYREGISTRO As Boolean? End Class Public Module CertificateLoginBridgeHelper Private ReadOnly DefaultCertificateHeaders As String() = { "X-ARR-ClientCert", "X-Client-Cert", "X-Client-Cert-Der-Base64", "X-Client-Certificate", "X-Client-Certificate-Der-Base64", "X-SSL-CERT", "X-SSL-Client-Cert", "X-SSL-Client-Cert-Base64", "X-Tls-Client-Cert", "X-Tls-Client-Cert-Der-Base64", "X-Forwarded-Tls-Client-Cert", "Ssl-Client-Cert" } Private ReadOnly DefaultCertificateServerVariables As String() = { "SSL_CLIENT_CERT", "HTTP_SSL_CLIENT_CERT", "HTTP_X_SSL_CLIENT_CERT", "HTTP_X_CLIENT_CERT", "HTTP_X_CLIENT_CERTIFICATE", "HTTP_X_ARR_CLIENTCERT", "CERT_CERTIFICATE" } Public Function IsCertificateHostRequest(request As HttpRequest, publicUrl As String) As Boolean If request Is Nothing OrElse String.IsNullOrWhiteSpace(publicUrl) Then Return False End If Dim configuredUri As Uri = Nothing If Not Uri.TryCreate(publicUrl, UriKind.Absolute, configuredUri) Then Return False End If Dim forwardedHostRaw = GetFirstHeaderValue(request, "X-Forwarded-Host") Dim forwardedProtoRaw = GetFirstHeaderValue(request, "X-Forwarded-Proto") Dim forwardedPortRaw = GetFirstHeaderValue(request, "X-Forwarded-Port") Dim hasForwardedInfo = Not String.IsNullOrWhiteSpace(forwardedHostRaw) OrElse Not String.IsNullOrWhiteSpace(forwardedProtoRaw) OrElse Not String.IsNullOrWhiteSpace(forwardedPortRaw) Dim requestHost = ExtractHost(If(hasForwardedInfo, forwardedHostRaw, Nothing)) If String.IsNullOrWhiteSpace(requestHost) AndAlso request.Url IsNot Nothing Then requestHost = request.Url.Host End If If Not String.Equals(requestHost, configuredUri.Host, StringComparison.OrdinalIgnoreCase) Then Return False End If If Not hasForwardedInfo Then Return True End If Dim requestScheme = FirstToken(forwardedProtoRaw) If String.IsNullOrWhiteSpace(requestScheme) Then requestScheme = configuredUri.Scheme End If Dim requestPort = ParseNullableInt(forwardedPortRaw) If Not requestPort.HasValue Then requestPort = ExtractPort(forwardedHostRaw) End If If Not requestPort.HasValue Then requestPort = GetDefaultPort(requestScheme) End If Dim configuredPort = If(configuredUri.IsDefaultPort, GetDefaultPort(configuredUri.Scheme), configuredUri.Port) Return String.Equals(requestScheme, configuredUri.Scheme, StringComparison.OrdinalIgnoreCase) AndAlso requestPort.Value = configuredPort End Function Public Function IsAllowedParentOrigin(parentOrigin As String, allowedParentOrigins As IEnumerable(Of String)) As Boolean Dim normalizedParentOrigin = NormalizeOrigin(parentOrigin) If String.IsNullOrWhiteSpace(normalizedParentOrigin) Then Return False End If Return allowedParentOrigins. Where(Function(origin) Not String.IsNullOrWhiteSpace(origin)). Select(Function(origin) NormalizeOrigin(origin)). Any(Function(origin) String.Equals(origin, normalizedParentOrigin, StringComparison.OrdinalIgnoreCase)) End Function Public Function ReadCertificate( request As HttpRequest, forwardedCertificateHeader As String, additionalForwardedCertificateHeaders As IEnumerable(Of String)) As X509Certificate2 Dim directCertificate = ReadDirectCertificate(request) If directCertificate IsNot Nothing Then Return directCertificate End If Dim headerCandidates As New List(Of String)() AddHeaderCandidate(headerCandidates, forwardedCertificateHeader) If additionalForwardedCertificateHeaders IsNot Nothing Then For Each headerName In additionalForwardedCertificateHeaders AddHeaderCandidate(headerCandidates, headerName) Next End If For Each headerName In DefaultCertificateHeaders AddHeaderCandidate(headerCandidates, headerName) Next For Each headerName In headerCandidates.Distinct(StringComparer.OrdinalIgnoreCase) Dim rawHeader = request.Headers(headerName) If String.IsNullOrWhiteSpace(rawHeader) Then Continue For End If Dim certificate = TryCreateCertificate(rawHeader) If certificate IsNot Nothing Then Return certificate End If Next For Each serverVariableName In DefaultCertificateServerVariables Dim rawValue = request.ServerVariables(serverVariableName) If String.IsNullOrWhiteSpace(rawValue) Then Continue For End If Dim certificate = TryCreateCertificate(rawValue) If certificate IsNot Nothing Then Return certificate End If Next Return Nothing End Function Public Function ObtenerDni(certificado As X509Certificate2) As String Try If certificado Is Nothing Then Return String.Empty End If Dim candidateValues As New List(Of String) From { ExtractDistinguishedNameValue(certificado.Subject, "OID.2.5.4.97"), ExtractDistinguishedNameValue(certificado.Subject, "2.5.4.97"), ExtractDistinguishedNameValue(certificado.Subject, "SERIALNUMBER"), ExtractDistinguishedNameValue(certificado.Subject, "OID.2.5.4.5"), ExtractDistinguishedNameValue(certificado.Subject, "2.5.4.5"), certificado.GetNameInfo(X509NameType.SimpleName, False) } For Each candidateValue In candidateValues Dim nif = NormalizarIdentificadorFiscal(candidateValue) If nif <> String.Empty Then Return nif End If Next Return NormalizarIdentificadorFiscal(certificado.Subject) Catch Return String.Empty End Try End Function Public Function BuildIframeDniHtml(dni As String, parentOrigin As String) As String Dim payloadJson = JsonConvert.SerializeObject(New With { .dni = dni }) Dim targetOriginJson = JsonConvert.SerializeObject(NormalizeOrigin(parentOrigin)) Return BuildIframePostMessageHtml( payloadJson, targetOriginJson, "Validando acceso con certificado...") End Function Public Function BuildIframeLoginHtml(response As CertificateProxyLoginResponse, parentOrigin As String) As String Dim payloadJson = JsonConvert.SerializeObject(New With { .token = response.Token, .user = response.User }) Dim targetOriginJson = JsonConvert.SerializeObject(NormalizeOrigin(parentOrigin)) Return BuildIframePostMessageHtml( payloadJson, targetOriginJson, "Validando acceso con certificado...") End Function Public Function BuildIframeErrorHtml(message As String, parentOrigin As String) As String Dim payloadJson = JsonConvert.SerializeObject(New With { .error = message }) Dim targetOriginJson = JsonConvert.SerializeObject(NormalizeOrigin(parentOrigin)) Dim safeMessage = If(String.IsNullOrWhiteSpace(message), "No se pudo completar el acceso con certificado.", message) Return BuildIframePostMessageHtml( payloadJson, targetOriginJson, safeMessage) End Function Private Function BuildIframePostMessageHtml( payloadJson As String, targetOriginJson As String, visibleMessage As String) As String Dim visibleMessageJson = JsonConvert.SerializeObject(visibleMessage) Return "" & "" & "Autenticacion Certificado" & "" & "
" & "" & "" & "" End Function Private Function ReadDirectCertificate(request As HttpRequest) As X509Certificate2 Try If request Is Nothing OrElse request.ClientCertificate Is Nothing OrElse Not request.ClientCertificate.IsPresent Then Return Nothing End If Dim rawCertificate = request.ClientCertificate.Certificate If rawCertificate Is Nothing OrElse rawCertificate.Length = 0 Then Return Nothing End If Return New X509Certificate2(rawCertificate) Catch Return Nothing End Try End Function Private Function TryCreateCertificate(rawHeader As String) As X509Certificate2 Dim normalizedValue = Uri.UnescapeDataString(rawHeader).Trim() If normalizedValue = String.Empty Then Return Nothing End If If normalizedValue.IndexOf("BEGIN CERTIFICATE", StringComparison.OrdinalIgnoreCase) >= 0 Then Try Dim pemPayload = Regex.Replace( normalizedValue, "-+BEGIN CERTIFICATE-+|-+END CERTIFICATE-+|\s+", String.Empty, RegexOptions.IgnoreCase) If pemPayload <> String.Empty Then Return New X509Certificate2(Convert.FromBase64String(pemPayload)) End If Catch End Try End If Dim compactValue = Regex.Replace(normalizedValue, "\s+", String.Empty) If compactValue.Length > 0 AndAlso compactValue.Length Mod 2 = 0 AndAlso Regex.IsMatch(compactValue, "^[0-9a-fA-F]+$") Then Try Return New X509Certificate2(HexStringToBytes(compactValue)) Catch End Try End If Try Return New X509Certificate2(Convert.FromBase64String(compactValue)) Catch Return Nothing End Try End Function Private Function NormalizarIdentificadorFiscal(value As String) As String If String.IsNullOrWhiteSpace(value) Then Return String.Empty End If Dim normalizedValue = value.Trim().Trim(""""c).ToUpperInvariant() If normalizedValue.StartsWith("IDCES-", StringComparison.OrdinalIgnoreCase) OrElse normalizedValue.StartsWith("VATES-", StringComparison.OrdinalIgnoreCase) Then normalizedValue = normalizedValue.Split("-"c).Last().Trim() End If Dim match = Regex.Match(normalizedValue, "\b[0-9XYZ]\d{7}[A-Z]\b", RegexOptions.IgnoreCase) If match.Success Then Return match.Value.ToUpperInvariant() End If Return String.Empty End Function Private Function ExtractDistinguishedNameValue(subject As String, attributeName As String) As String If String.IsNullOrWhiteSpace(subject) OrElse String.IsNullOrWhiteSpace(attributeName) Then Return String.Empty End If Dim pattern = "(?:^|,\s*)" & Regex.Escape(attributeName) & "\s*=\s*(""[^""]+""|[^,]+)" Dim match = Regex.Match(subject, pattern, RegexOptions.IgnoreCase) If Not match.Success Then Return String.Empty End If Return match.Groups(1).Value.Trim().Trim(""""c) End Function Private Function HexStringToBytes(value As String) As Byte() Dim bytes((value.Length \ 2) - 1) As Byte For i = 0 To bytes.Length - 1 bytes(i) = Convert.ToByte(value.Substring(i * 2, 2), 16) Next Return bytes End Function Private Function NormalizeOrigin(value As String) As String Dim uri As Uri = Nothing If Not Uri.TryCreate(value, UriKind.Absolute, uri) Then Return String.Empty End If Return uri.GetLeftPart(UriPartial.Authority).TrimEnd("/"c) End Function Private Function ExtractHost(value As String) As String Dim token = FirstToken(value) If String.IsNullOrWhiteSpace(token) Then Return String.Empty End If Dim uri As Uri = Nothing If Uri.TryCreate("http://" & token, UriKind.Absolute, uri) Then Return uri.Host End If Return token End Function Private Function ExtractPort(value As String) As Integer? Dim token = FirstToken(value) If String.IsNullOrWhiteSpace(token) Then Return Nothing End If Dim uri As Uri = Nothing If Uri.TryCreate("http://" & token, UriKind.Absolute, uri) AndAlso Not uri.IsDefaultPort Then Return uri.Port End If Return Nothing End Function Private Function ParseNullableInt(value As String) As Integer? Dim parsedValue As Integer If Integer.TryParse(FirstToken(value), parsedValue) Then Return parsedValue End If Return Nothing End Function Private Function FirstToken(value As String) As String If String.IsNullOrWhiteSpace(value) Then Return String.Empty End If Return value.Split(","c)(0).Trim() End Function Private Function GetFirstHeaderValue(request As HttpRequest, headerName As String) As String If request Is Nothing OrElse String.IsNullOrWhiteSpace(headerName) Then Return String.Empty End If Return request.Headers(headerName) End Function Private Function GetDefaultPort(scheme As String) As Integer If String.Equals(scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) Then Return 80 End If Return 443 End Function Private Sub AddHeaderCandidate(headerCandidates As ICollection(Of String), headerName As String) If Not String.IsNullOrWhiteSpace(headerName) Then headerCandidates.Add(headerName.Trim()) End If End Sub End Module End Namespace