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.
- The user opens your login URL. Your application builds an AuthnRequest and redirects the browser to the IdP.
- The IdP signs the user in, with whatever password, MFA or conditional access rules the company has.
- The IdP answers with a signed SAMLResponse, and the browser posts it to your Assertion Consumer Service (ACS) URL.
- 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.
- Describe your application. Set
EntityID, the unique name of your application (usually its metadata URL), andAssertionConsumerServiceURL, the https URL where the response arrives. - Describe the identity provider. Call
LoadIdPMetadatawith the IdP metadata document. It reads the IdP entity ID, its sign-in URL and binding, and every signing certificate. Without metadata, setIdPEntityID,IdPSSOURLandIdPCertificatesby hand. - Send the request.
GetAuthnRequestRedirectURLreturns the URL to redirect the browser to. For an IdP that only offers the HTTP-POST binding,GetAuthnRequestPostFormreturns a page that posts the request instead. - Keep the request id. Both methods return the id of the new AuthnRequest. Store it on the server, keyed by a random RelayState or by the session cookie, and remove it when the answer arrives, so every request can be answered only once.
- Process the response. At the ACS URL, call
ProcessResponsewith the posted SAMLResponse, the RelayState and the stored request id. When it returnsTrue, aTsgcSAMLResultholds theNameID, theSessionIndexand every attribute the IdP sent. When it returnsFalse,ErrorMessagesays why andOnSAMLErrorfires.
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.
- Microsoft Entra ID. Enterprise applications, New application, Create your own application (non-gallery). In Single sign-on choose SAML, then upload the SP metadata or fill Identifier (Entity ID) and Reply URL. Assign users or groups, and load the App Federation Metadata Url shown in SAML Certificates.
- Okta. Applications, Create App Integration, SAML 2.0. Single sign-on URL is your ACS URL, with “Use this for Recipient URL and Destination URL” checked, and Audience URI is your entity ID. Add attribute statements such as email, firstName and lastName, assign people or groups, and load the Metadata URL from the Sign On tab.
- AD FS. Add a claims aware Relying Party Trust and import the SP metadata. AD FS only accepts https endpoints. Add claim rules that send a Name ID, for example E-Mail-Addresses sent as E-Mail Address, then E-Mail Address transformed into Name ID. The IdP metadata is at
https://<adfs-host>/FederationMetadata/2007-06/FederationMetadata.xml. - Google Workspace. Admin console, Apps, Web and mobile apps, Add custom SAML app. Download the IdP metadata, enter your ACS URL and entity ID, choose the Name ID (for example the primary email) and turn the app on for your users.
- Keycloak. Create a SAML client whose Client ID is your entity ID, or import the SP metadata. Keycloak signs the whole document by default, so enable Sign assertions as well. If Client signature required is on, set
SignAuthnRequests,SPCertificateandSPPrivateKey. The IdP metadata is athttps://<host>/realms/<realm>/protocol/saml/descriptor.
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 signature, against the IdP certificate. The response is verified only with the certificates in
IdPCertificates. A certificate embedded in the message is never trusted, because an attacker can embed one too. WithWantAssertionsSigned, the default, the assertion must carry its own signature. - Signature wrapping defence. The signature must reference an element whose ID is unique in the document, and after the verification only the signed element is read. An unsigned assertion slipped in next to the signed one is never looked at.
- A single assertion. The response must contain exactly one assertion, directly under the response.
- Audience and recipient. The audience must be your
EntityIDand the recipient yourAssertionConsumerServiceURL, so an assertion issued for another application is refused. - The time window. NotBefore and NotOnOrAfter are checked against UTC with a tolerance of
ClockSkewseconds, two minutes by default.MaxAssertionAgecan also limit how old an assertion may be. - InResponseTo. The response must answer the request id you stored. Unsolicited, IdP-initiated responses are refused unless you set
AllowIdPInitiated. - Replay cache. The ID of every accepted assertion is kept until it expires, so the same response posted twice is refused. The cache is thread safe and lives in memory. When several servers share the sign-in, override
DoAddToReplayCacheto keep the IDs in a shared store. - SHA-1 off by default. RSA-SHA1 signatures and SHA-1 digests are refused unless you set
AllowSHA1for an IdP that still needs them.
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:
- 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.
- Click Load IdP metadata. The default source is the mocksaml.com metadata URL.
- Click Start, then Open Browser, and follow the sign in link.
- On mocksaml.com, type any user name on the example.com domain and any password.
- 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
- No encrypted assertions. A response with an EncryptedAssertion or an encrypted NameID is refused. Leave assertion encryption disabled in the IdP. The assertion is still signed and travels over https.
- No Single Logout. SLO is not implemented.
SessionIndexis returned so your application can end its own session and build its own logout.
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.
Read Next
- Delphi Login With Passkeys, SAML SSO, LDAP and TOTP 2FA
- Delphi PKCE OAuth2
- Authorization using PassKeys
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.
