SAML Single Sign-On in Delphi With Entra ID, Okta and AD FS

· Components
SAML Single Sign-On in Delphi With Entra ID, Okta and AD FS

Sooner or later a large customer asks the question: can our people sign in to your application with their company account? They do not mean one more user name and password. They mean the Microsoft Entra ID, Okta or AD FS login they already use for everything else, with their own password policy, their own two-factor and one place to switch an account off the day someone leaves.

The answer their identity team expects is SAML 2.0. In the overview of the new login components SAML got one paragraph. This post is the whole flow: what TsgcSAMLServiceProvider does, the code of a login page and an Assertion Consumer Service, how to register your application with the common identity providers, and how to test all of it today without an account anywhere.

How SAML Sign-In Works

Three parties take part. Your application is the service provider (SP). The customer's directory is the identity provider (IdP). The browser carries the messages between them, so your server and the IdP never talk to each other directly.

  1. The user opens your login URL. Your application builds an AuthnRequest and redirects the browser to the IdP.
  2. The IdP signs the user in, with whatever password, MFA or conditional access rules the company has.
  3. The IdP answers with a signed SAMLResponse, and the browser posts it to your Assertion Consumer Service (ACS) URL.
  4. Your application validates the response and creates its own session for the user it names.

Step four is where SAML implementations go wrong, and it is the part the component does for you.

The Service Provider, Step by Step

TsgcSAMLServiceProvider is not an HTTP server. It builds and checks the SAML messages, and you call it from the request handler of the server your application already has, for example a TsgcWebSocketHTTPServer or a TsgcHTTPServer.

With EntityID, AssertionConsumerServiceURL and LoadIdPMetadata done once at startup, the login page and the ACS fit in one request handler:

uses
  sgcAuth_SAML_SP;

procedure TMyApp.OnCommandGet(AContext: TIdContext;
  ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
  vRelayState, vRequestID: string;
  oResult: TsgcSAMLResult;
begin
  if ARequestInfo.Document = '/saml/login' then
  begin
    // 1. send the browser to the identity provider
    vRelayState := NewRelayState;
    AResponseInfo.Redirect(FSAML.GetAuthnRequestRedirectURL(vRelayState,
      vRequestID));
    // 2. keep the request id, the response must answer it
    AddPendingRequest(vRelayState, vRequestID);
  end
  else if (ARequestInfo.Document = '/saml/acs') and
    SameText(ARequestInfo.Command, 'POST') then
  begin
    // 3. the browser posts SAMLResponse and RelayState back
    vRelayState := ARequestInfo.Params.Values['RelayState'];
    vRequestID := TakePendingRequest(vRelayState);
    oResult := TsgcSAMLResult.Create;
    try
      if FSAML.ProcessResponse(ARequestInfo.Params.Values['SAMLResponse'],
        vRelayState, vRequestID, oResult) then
      begin
        // 4. signed in: create your own session for this user
        CreateUserSession(AResponseInfo, oResult.NameID, oResult.SessionIndex);
        AResponseInfo.Redirect('/');
      end
      else
        AResponseInfo.ResponseNo := 403; // log oResult.ErrorMessage
    finally
      oResult.Free;
    end;
  end;
end;

NewRelayState, AddPendingRequest, TakePendingRequest and CreateUserSession stand for your own code: a GUID, a thread safe list keyed by RelayState that hands each request id out once, and the session cookie of your application. Attributes arrive as Name=Value lines, so oResult.Attributes.Values['email'] reads one by name. Entra ID names them with claim URIs such as http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress.

The RelayState is not covered by the IdP signature. Use it as a key to find your own pending request, never as a URL you redirect to without checking it.

Register Your Application With the Identity Provider

GetMetadata returns the service provider metadata: your entity ID and your ACS URL with the HTTP-POST binding. Serve it at a URL such as /saml/metadata, or save it to a file, and give it to the IdP. Every identity provider asks for the same two values, the SP entity ID and the ACS URL, so the notes below are mostly about where each console keeps them. In all of them, leave assertion encryption switched off.

Whatever the IdP, the last step is the same: pass its metadata to LoadIdPMetadata. When the document describes several entities, the second parameter picks yours.

What ProcessResponse Checks

A SAML response is a signed XML document, and most of the well known SAML vulnerabilities are ways to make a service provider read something other than what was signed. A response is accepted only when every one of these checks passes:

The parser also refuses DOCTYPE declarations, so there are no external entities, and it limits the size and nesting depth of the document. The issuer must be the IdP you configured. The first failed check stops the validation and its reason is in ErrorMessage: log it, and show the user a plain “sign in failed” page.

Try It Without an Account

You do not need an Entra ID tenant to see SAML working. Mock SAML is a free test identity provider at mocksaml.com. It accepts any service provider and takes the audience and the ACS URL from the AuthnRequest, so there is nothing to register.

The demo Demos\26.Authentication\03.SAML_ServiceProvider is a complete service provider on a TsgcWebSocketHTTPServer, with /login, /acs and /metadata endpoints on http://localhost:8090:

  1. Build the demo and keep libcrypto-3.dll and libssl-3.dll next to the executable. They are in the demo folder, and OpenSSL verifies the RSA signatures.
  2. Click Load IdP metadata. The default source is the mocksaml.com metadata URL.
  3. Click Start, then Open Browser, and follow the sign in link.
  4. On mocksaml.com, type any user name on the example.com domain and any password.
  5. The browser comes back to the ACS, and the page shows the NameID, the SessionIndex and the attributes id, email, firstName and lastName.

When that works, open http://localhost:8090/metadata, register it with your real IdP, load the IdP metadata in the demo and sign in again. For AD FS, run the demo with SSL first, because AD FS only accepts https.

Current Limits

Documentation

Where to Get It

TsgcSAMLServiceProvider is included in the Enterprise and All-Access editions of sgcWebSockets, for Delphi and C++ Builder, and the same component is part of sgcWebSockets .NET. If you only need authentication, the sgcAuth pack has it with the other login components. The unit is sgcAuth_SAML_SP, and nothing changes in an existing application until you drop the component on a form.

Watch It

There is a short video, “SAML single sign-on in Delphi with Entra ID, Okta and AD FS”, on the eSeGeCe channel. It shows the code in the IDE and a live sign-in with the demo against mocksaml.com.

Questions, feedback or help connecting your identity provider? Get in touch. You will get a reply from the people who wrote the code.