SAML2
#139
Replies: 2 comments 2 replies
-
Saml2 generally does not work well with native/desktop applications. I would use IdentityServer as a protocol bridge. Set up your application to authenticate using OIDC to IdentityServer. Then use a Saml2 authentication handler to connect IdentityServer to the Saml2 Idp. The Sustainsys.Saml2.AspNetCore2 package is free to use and compatible with IdentityServer. |
Beta Was this translation helpful? Give feedback.
1 reply
-
I would like to use the sustainsys.saml2 library to authenticate a desktop windows forms application (written in c#) with the help of the system browser by acquiring the response and the redirect with a small https:localhost:port listening.
My idp is managed by keycloack hosted into Ubuntu app of windows.

in keycloack I configured a realm with SAML 2.0 Identity Provider Metadata endpoint.

<https://gguaita-2015.objectway.it:8443/realms/gguaita-2015-saml/protocol/saml/descriptor> gguaita-2015:8443/realms/gguaita-2015-saml/protocol/saml/descriptor
<md:EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds=http://www.w3.org/2000/09/xmldsig# <http://www.w3.org/2000/09/xmldsig> entityID=https://gguaita-2015:8443/realms/gguaita-2015-saml>
<md:IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:KeyDescriptor use="signing">
<ds:KeyInfo>
<ds:KeyName>MgcsLLrL1BaGg78br8vwLrXdHBor272KN2rhk9EIK3k</ds:KeyName>
<ds:X509Data>
<ds:X509Certificate>MIICsTCCAZkCBgGVlAwPOjANBgkqhki...</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location=https://gguaita-2015:8443/realms/gguaita-2015-saml/protocol/saml/resolve index="0"/>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location=https://gguaita-2015:8443/realms/gguaita-2015-saml/protocol/saml/>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location=https://gguaita-2015:8443/realms/gguaita-2015-saml/protocol/saml/>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location=https://gguaita-2015:8443/realms/gguaita-2015-saml/protocol/saml/>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location=https://gguaita-2015:8443/realms/gguaita-2015-saml/protocol/saml/>
<md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat>
<md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location=https://gguaita-2015:8443/realms/gguaita-2015-saml/protocol/saml/>
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location=https://gguaita-2015:8443/realms/gguaita-2015-saml/protocol/saml/>
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location=https://gguaita-2015:8443/realms/gguaita-2015-saml/protocol/saml/>
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location=https://gguaita-2015:8443/realms/gguaita-2015-saml/protocol/saml/>
</md:IDPSSODescriptor>
</md:EntityDescriptor>
I’m setting with redirect url a small https server on localhost:port to acquire the redirect after login.

I'm trying to write some code to create the SAMLRequest and execute it with a ProcesSstartInfo from the system browser bat I can't find any simple documentation to complete it.
var saml2Options = new Saml2Options
{
SPOptions = new SPOptions
{
EntityId = new EntityId($@ <mailto:$@%22_%7bGuid.NewGuid()%7d> "_{Guid.NewGuid()}"),
ReturnUrl = new Uri(targetUrl)
}
};
var idp = new IdentityProvider(new EntityId("http://gguaita-2015:8090/realms/gguaita-2015-saml/protocol/saml"), saml2Options.SPOptions)
{
Binding = Sustainsys.Saml2.WebSso.Saml2BindingType.HttpPost,
MetadataLocation = "http://gguaita-2015:8090/realms/gguaita-2015-saml/protocol/saml/description",
AllowUnsolicitedAuthnResponse = true,
LoadMetadata = true
};
saml2Options.IdentityProviders.Add(idp);
???
???
???
Process p = new Process();
p.StartInfo = new ProcessStartInfo
{
FileName = "https://gguaita-2015:8443/realms/gguaita-2015-saml/protocol/saml/?SAMLRequest=???",
UseShellExecute = true
};
p.Start();
The next step will be to receive the response at https://localhost:port and process it to extract the validation token.
I attach a "bad code" that runs this flow and captures the login information entered below, but I wanted to handle request and response with sustainsys library… or is this not possible?

thanks for your attention.
Giorgio.
From: Anders Abel ***@***.***>
Sent: lunedì 31 marzo 2025 11:43
To: DuendeSoftware/community ***@***.***>
Cc: gguaita ***@***.***>; Author ***@***.***>
Subject: Re: [DuendeSoftware/community] SAML2 (Discussion #139)
Saml2 generally does not work well with native/desktop applications. I would use IdentityServer as a protocol bridge. Set up your application to authenticate using OIDC to IdentityServer. Then use a Saml2 authentication handler to connect IdentityServer to the Saml2 Idp. The Sustainsys.Saml2.AspNetCore2 package is free to use and compatible with IdentityServer.
—
Reply to this email directly, view it on GitHub <#139 (comment)> , or unsubscribe <https://github.com/notifications/unsubscribe-auth/BE2LY2OTO5V5ZGTWNWXQ2WL2XEE2XAVCNFSM6AAAAABZ77475KVHI2DSMVQWIX3LMV43URDJONRXK43TNFXW4Q3PNVWWK3TUHMYTENRXGQ2DMMQ> .
You are receiving this because you authored the thread. <https://github.com/notifications/beacon/BE2LY2N5ZVWQZ7T6L3XX2OL2XEE2XA5CNFSM6AAAAABZ77475KWGG33NNVSW45C7OR4XAZNRIRUXGY3VONZWS33OINXW23LFNZ2KUY3PNVWWK3TUL5UWJTQAYFSZ4.gif> Message ID: ***@***.*** ***@***.***> >
// MainForm.cs (for .NET Framework 4.8)
using HttpServerLite;
using Microsoft.Web.WebView2.Core;
using System;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Security.Cryptography.Pkcs;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography.Xml;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Xml;
using System.Xml.Linq;
using Sustainsys.Saml2;
using Sustainsys.Saml2.Configuration;
using Sustainsys.Saml2.WebSso;
using Sustainsys.Saml2.Metadata;
using Sustainsys.Saml2.Saml2P;
using Sustainsys.Saml2.AspNetCore2;
using Sustainsys.Saml2.Owin;
using System.Web.Hosting;
using System.Security.Policy;
using Microsoft.Extensions.Options;
using Sustainsys.Saml2.Exceptions;
namespace nsSaml2
{
public partial class Form1 : Form
{
private string saml2Endpoint = "https://gguaita-2015:8443/realms/gguaita-2015-saml/protocol/saml/"; // Replace with your SAML2 endpoint
private string relayState = null; // Optional: Replace with your relay state
private string targetUrl = "https://localhost:5000"; // Replace with your application's target URL after authentication
private string acsUrl = "https://localhost:5000"; //Replace with your Assertion Consumer Service URL
private string issuer = "gguaitaSaml"; //Replace with your issuer.
private string aRedirecProtocol = "https";
private int aRedirecPort = 5000;
private string aClientSecret = "WyC45ism2iXoiHBWuWaO6Qnu2NCNQ6oV";
static private string raw = null;
public Form1()
{
InitializeComponent();
Authentication();
Callback();
}
private void Callback()
{
Webserver server = null;
if (aRedirecProtocol.Equals("https"))
{
X509Certificate2 cert = GetExistingCertificate("localhost");
if (cert == null || cert.NotAfter <= DateTime.Now)
{
if (cert != null) { RemoveCertificate(cert); }
if (System.IO.File.Exists("certificate.pfx"))
{
server = new HttpServerLite.Webserver("localhost", aRedirecPort, true, "certificate.pfx", "password", DefaultRoute);
}
else
{
cert = CreateAndInstallCertificate("localhost", aClientSecret, false);
server = new HttpServerLite.Webserver("localhost", aRedirecPort, DefaultRoute, cert);
}
}
}
else
{
server = new HttpServerLite.Webserver("localhost", aRedirecPort, false, null, null, DefaultRoute);
}
server.Start();
while (raw == null) { Task task = Task.Delay(3000); }
raw = System.Web.HttpUtility.UrlDecode(raw);
byte[] samlData = Convert.FromBase64String(raw.Substring(13));
string samldata = Encoding.UTF8.GetString(samlData);
string firstName = string.Empty;
XmlDocument xDoc = new XmlDocument();
samldata = samldata.Replace(@"\", "");
xDoc.LoadXml(samldata);
XmlNamespaceManager xMan = new XmlNamespaceManager(xDoc.NameTable);
xMan.AddNamespace("samlp", "urn:oasis:names:tc:SAML:2.0:protocol");
xMan.AddNamespace("saml", "urn:oasis:names:tc:SAML:2.0:assertion");
xMan.AddNamespace("ds", "http://www.w3.org/2000/09/xmldsig#");
XmlNode xNode = ***@***.***", xMan);
if (xNode != null)
{
string statusCode = xNode.Value;
if (statusCode.EndsWith("status:Success"))
{
this.listBox1.Items.Add("AuthenticationStatus: true");
}
else
{
this.listBox1.Items.Add("AuthenticationStatus: " + statusCode);
}
}
else
{
this.listBox1.Items.Add("AuthenticationStatus: null");
}
xNode = ***@***.***", xMan);
if (xNode != null)
{
this.listBox1.Items.Add("Destination: " + xNode.Value);
}
xNode = ***@***.***", xMan);
if (xNode != null)
{
this.listBox1.Items.Add("AutheticationTime: " + Convert.ToDateTime(xNode.Value).ToString());
}
xNode = ***@***.***", xMan);
if (xNode != null)
{
this.listBox1.Items.Add("ResponseID: " + xNode.Value);
}
xNode = xDoc.SelectSingleNode("/samlp:Response/saml:Issuer", xMan);
if (xNode != null)
{
this.listBox1.Items.Add("Issuer: " + xNode.InnerText);
}
xNode = xDoc.SelectSingleNode("/samlp:Response/ds:Signature/ds:SignedInfo/ds:Reference/ds:DigestValue", xMan);
if (xNode != null)
{
this.listBox1.Items.Add("SignatureReferenceDigestValue: " + xNode.InnerText);
}
xNode = xDoc.SelectSingleNode("/samlp:Response/ds:Signature/ds:SignatureValue", xMan);
if (xNode != null)
{
this.listBox1.Items.Add("SignatureValue: " + xNode.InnerText);
}
xNode = ***@***.***", xMan);
if (xNode != null)
{
this.listBox1.Items.Add("AuthenticationSession: " + xNode.Value);
}
xNode = xDoc.SelectSingleNode("/samlp:Response/saml:Assertion/saml:Subject/saml:NameID", xMan);
if (xNode != null)
{
this.listBox1.Items.Add("SubjectNameID: " + xNode.InnerText);
}
xNode = xDoc.SelectSingleNode("/samlp:Response/saml:Assertion/saml:Conditions/saml:AudienceRestriction/saml:Audience", xMan);
if (xNode != null)
{
this.listBox1.Items.Add("Audience: " + xNode.InnerText);
}
//reverse order
//</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion></samlp:Response>
//string xQryStr = "//NewPatient[Name='" + name + "']";
//XmlNode matchedNode = xDoc.SelectSingleNode(xQryStr);
// samlp:Response saml:Assertion saml:AttributeStatement saml:Attribute
xNode = ***@***.*** = 'givenName']/saml:AttributeValue", xMan);
if (xNode != null)
{
this.listBox1.Items.Add("FirstName: " + xNode.InnerText);
}
// samlp:Response saml:Assertion saml:AttributeStatement saml:Attribute
xNode = ***@***.*** = 'email']/saml:AttributeValue", xMan);
if (xNode != null)
{
this.listBox1.Items.Add("Mail: " + xNode.InnerText);
}
// samlp:Response saml:Assertion saml:AttributeStatement saml:Attribute
xNode = ***@***.*** = 'surname']/saml:AttributeValue", xMan);
if (xNode != null)
{
this.listBox1.Items.Add("LastName: " + xNode.InnerText);
}
}
private void Authentication()
{
this.listBox1.Items.Clear();
string samlRequest = GenerateSamlRequest();
string samlUrl = $"{saml2Endpoint}?SAMLRequest={Uri.EscapeDataString(samlRequest)}";
this.listBox1.Items.Add("RequestID: " + requestID);
Process p = new Process();
p.StartInfo = new ProcessStartInfo
{
FileName = samlUrl,
UseShellExecute = true
};
p.Start();
}
string requestID = string.Empty;
private string GenerateSamlRequest()
{
// Implement your SAML request generation logic here.
// This is a simplified example; you'll need a full SAML library
// for production use.
requestID = $@"_{Guid.NewGuid()}";
string samlRequest = $@"<samlp:AuthnRequest xmlns:samlp=""urn:oasis:names:tc:SAML:2.0:protocol""
xmlns:saml=""urn:oasis:names:tc:SAML:2.0:assertion""
ID=""{requestID}""
Version=""2.0""
IssueInstant=""{DateTime.UtcNow:yyyy-MM-ddTHH:mm:ssZ}""
ProtocolBinding=""urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST""
AssertionConsumerServiceURL=""{acsUrl}"">
<saml:Issuer>{issuer}</saml:Issuer>
<samlp:NameIDPolicy Format=""urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified""
AllowCreate=""true""/>
<samlp:RequestedAuthnContext Comparison=""exact"">
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>
</samlp:RequestedAuthnContext>
</samlp:AuthnRequest>";
XmlDocument samlDoc = new XmlDocument();
samlDoc.PreserveWhitespace = true;
samlDoc.LoadXml(samlRequest);
byte[] bytes = Encoding.UTF8.GetBytes(samlDoc.OuterXml);
using (MemoryStream memoryStream = new MemoryStream())
{
using (DeflateStream deflateStream = new DeflateStream(memoryStream, CompressionLevel.Optimal))
{
deflateStream.Write(bytes, 0, bytes.Length);
}
bytes = memoryStream.ToArray();
}
return Convert.ToBase64String(bytes);
}
private void CoreWebView2_NavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs e)
{
Debug.WriteLine($"Navigation Starting: {e.Uri}");
}
private void CoreWebView2_NavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs e)
{
if (e.IsSuccess)
{
Debug.WriteLine($"Navigation Completed: {webView21.Source}");
if (webView21.Source.ToString().StartsWith(targetUrl, StringComparison.OrdinalIgnoreCase))
{
string authenticatedUrl = webView21.Source.ToString();
MessageBox.Show($"Authentication Successful: {authenticatedUrl}");
}
else
{
string url = webView21.Source.ToString();
if (url.Contains("SAMLResponse"))
{
ProcessSamlResponse();
}
}
}
else
{
MessageBox.Show($"Navigation failed: {e.HttpStatusCode}");
}
}
private async void ProcessSamlResponse()
{
string javascript = @"
const urlParams = new URLSearchParams(window.location.search);
const samlResponse = urlParams.get('SAMLResponse');
if (samlResponse) {
window.chrome.webview.postMessage(samlResponse);
}
";
webView21.CoreWebView2.WebMessageReceived += CoreWebView2_WebMessageReceived;
await webView21.CoreWebView2.ExecuteScriptAsync(javascript);
}
private void CoreWebView2_WebMessageReceived(object sender, CoreWebView2WebMessageReceivedEventArgs e)
{
string samlResponse = e.WebMessageAsJson;
webView21.CoreWebView2.WebMessageReceived -= CoreWebView2_WebMessageReceived;
MessageBox.Show($"SAML Response Received: {samlResponse}");
webView21.CoreWebView2.Navigate(targetUrl);
}
private async Task DefaultRoute(HttpContext ctx)
{
raw = ctx.Request.DataAsString;
string resp = DefaultHtml();
ctx.Response.StatusCode = 200;
ctx.Response.ContentType = "text/html";
await ctx.Response.SendAsync(resp);
}
static string DefaultHtml()
{
using (var ms = new MemoryStream())
{
using (var bitmap = Properties.Resources.logo_symbol_color_small)
{
bitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
var logoow = Convert.ToBase64String(ms.GetBuffer()); //Get Base64
return
"<html>" +
"<head><title>Saml</title></head>" +
"<body>" +
"<h2 style=\"position: absolute; top: 100; left: 30\">Complete...</h2>" +
"</body>" +
"</html>";
}
}
}
}
}
|
Beta Was this translation helpful? Give feedback.
1 reply
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
Have you written code for single signon, from a desktop application c# Framework .NET 4.8, with SAML2?
Beta Was this translation helpful? Give feedback.
All reactions