Mais cedo ou mais tarde, um cliente grande faz a pergunta: nossa equipe pode fazer login na sua aplicação com a conta corporativa? Eles não estão falando de mais um nome de usuário e senha. Estão falando do login do Microsoft Entra ID, Okta ou AD FS que já usam para tudo o mais, com a própria política de senhas, o próprio segundo fator e um único lugar para desativar uma conta no dia em que alguém sai da empresa.
A resposta que a equipe de identidade deles espera é SAML 2.0. Na visão geral dos novos componentes de login, SAML ganhou apenas um parágrafo. Este post é o fluxo completo: o que TsgcSAMLServiceProvider faz, o código de uma página de login e de um Assertion Consumer Service, como registrar sua aplicação nos identity providers mais comuns, e como testar tudo isso hoje mesmo sem ter conta em lugar nenhum.
Como funciona o login SAML
Três partes participam. Sua aplicação é o service provider (SP). O diretório do cliente é o identity provider (IdP). O navegador transporta as mensagens entre os dois, de modo que seu servidor e o IdP nunca conversam diretamente.
- O usuário abre a URL de login. Sua aplicação monta um AuthnRequest e redireciona o navegador para o IdP.
- O IdP autentica o usuário, seguindo as regras de senha, MFA ou acesso condicional que a empresa tiver definido.
- O IdP responde com um SAMLResponse assinado, e o navegador o envia via POST para a URL do seu Assertion Consumer Service (ACS).
- Sua aplicação valida a resposta e cria sua própria sessão para o usuário que ela indica.
O passo quatro é onde as implementações de SAML erram, e é exatamente a parte que o componente faz por você.
O service provider, passo a passo
TsgcSAMLServiceProvider não é um servidor HTTP. Ele monta e verifica as mensagens SAML, e você o chama a partir do request handler do servidor que sua aplicação já possui, por exemplo um TsgcWebSocketHTTPServer ou um TsgcHTTPServer.
- Descreva sua aplicação. Defina
EntityID, o nome único da sua aplicação (geralmente a URL dos seus metadados), eAssertionConsumerServiceURL, a URL https onde a resposta chega. - Descreva o identity provider. Chame
LoadIdPMetadatacom o documento de metadados do IdP. Ele lê o entity ID do IdP, sua URL de login e binding, e todos os certificados de assinatura. Sem metadados, defina manualmenteIdPEntityID,IdPSSOURLeIdPCertificates. - Envie a solicitação.
GetAuthnRequestRedirectURLretorna a URL para a qual redirecionar o navegador. Para um IdP que só oferece o binding HTTP-POST,GetAuthnRequestPostFormretorna em vez disso uma página que envia a solicitação via POST. - Guarde o id da solicitação. Os dois métodos retornam o id do novo AuthnRequest. Guarde-o no servidor, indexado por um RelayState aleatório ou pelo cookie de sessão, e remova-o quando a resposta chegar, para que cada solicitação possa ser respondida apenas uma vez.
- Processe a resposta. Na URL do ACS, chame
ProcessResponsecom o SAMLResponse recebido, o RelayState e o id de solicitação guardado. Quando retornaTrue, umTsgcSAMLResultcontém oNameID, oSessionIndexe cada atributo enviado pelo IdP. Quando retornaFalse,ErrorMessageexplica o motivo eOnSAMLErroré disparado.
Com EntityID, AssertionConsumerServiceURL e LoadIdPMetadata definidos uma vez na inicialização, a página de login e o ACS cabem em um único 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 e CreateUserSession representam seu próprio código: um GUID, uma lista thread safe indexada por RelayState que entrega cada id de solicitação apenas uma vez, e o cookie de sessão da sua aplicação. Os atributos chegam como linhas Name=Value, então oResult.Attributes.Values['email'] lê um pelo nome. O Entra ID os nomeia com claim URIs como http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress.
O RelayState não está coberto pela assinatura do IdP. Use-o como chave para encontrar sua própria solicitação pendente, nunca como uma URL para a qual você redireciona sem verificar.
Registre sua aplicação no identity provider
GetMetadata retorna os metadados do service provider: seu entity ID e sua URL de ACS com o binding HTTP-POST. Sirva-os em uma URL como /saml/metadata, ou salve-os em um arquivo, e entregue-os ao IdP. Todo identity provider pede os mesmos dois valores, o entity ID do SP e a URL do ACS, então as notas abaixo tratam principalmente de onde cada console guarda esses dados. Em todos eles, deixe a criptografia de asserções desativada.
- Microsoft Entra ID. Enterprise applications, New application, Create your own application (non-gallery). Em Single sign-on escolha SAML, e então envie os metadados do SP ou preencha Identifier (Entity ID) e Reply URL. Atribua usuários ou grupos, e carregue a App Federation Metadata Url exibida em SAML Certificates.
- Okta. Applications, Create App Integration, SAML 2.0. Single sign-on URL é sua URL de ACS, com “Use this for Recipient URL and Destination URL” marcado, e Audience URI é seu entity ID. Adicione attribute statements como email, firstName e lastName, atribua pessoas ou grupos, e carregue o Metadata URL a partir da aba Sign On.
- AD FS. Adicione um claims aware Relying Party Trust e importe os metadados do SP. O AD FS só aceita endpoints https. Adicione claim rules que enviem um Name ID, por exemplo E-Mail-Addresses enviado como E-Mail Address, e depois E-Mail Address transformado em Name ID. Os metadados do IdP estão em
https://<adfs-host>/FederationMetadata/2007-06/FederationMetadata.xml. - Google Workspace. Admin console, Apps, Web and mobile apps, Add custom SAML app. Baixe os metadados do IdP, informe sua URL de ACS e o entity ID, escolha o Name ID (por exemplo o e-mail principal) e ative o app para seus usuários.
- Keycloak. Crie um SAML client cujo Client ID seja seu entity ID, ou importe os metadados do SP. O Keycloak assina o documento inteiro por padrão, então ative também Sign assertions. Se Client signature required estiver ativo, defina
SignAuthnRequests,SPCertificateeSPPrivateKey. Os metadados do IdP estão emhttps://<host>/realms/<realm>/protocol/saml/descriptor.
Qualquer que seja o IdP, o último passo é o mesmo: passe os metadados dele para LoadIdPMetadata. Quando o documento descreve várias entidades, o segundo parâmetro escolhe a sua.
O que ProcessResponse verifica
Uma resposta SAML é um documento XML assinado, e a maioria das vulnerabilidades SAML conhecidas são formas de fazer um service provider ler algo diferente do que foi assinado. Uma resposta só é aceita quando todas estas verificações passam:
- A assinatura, contra o certificado do IdP. A resposta é verificada apenas com os certificados em
IdPCertificates. Um certificado embutido na mensagem nunca é confiável, porque um atacante também pode embutir um. ComWantAssertionsSigned, o padrão, a asserção precisa ter sua própria assinatura. - Defesa contra signature wrapping. A assinatura precisa referenciar um elemento cujo ID seja único no documento, e após a verificação apenas o elemento assinado é lido. Uma asserção não assinada colocada ao lado da assinada nunca é considerada.
- Uma única asserção. A resposta precisa conter exatamente uma asserção, diretamente sob a response.
- Audience e recipient. A audience precisa ser seu
EntityIDe o recipient suaAssertionConsumerServiceURL, de modo que uma asserção emitida para outra aplicação seja recusada. - A janela de tempo. NotBefore e NotOnOrAfter são verificados contra UTC com uma tolerância de
ClockSkewsegundos, dois minutos por padrão.MaxAssertionAgetambém pode limitar a idade máxima de uma asserção. - InResponseTo. A resposta precisa responder ao id de solicitação que você guardou. Respostas não solicitadas, iniciadas pelo IdP, são recusadas a menos que você defina
AllowIdPInitiated. - Cache de replay. O ID de cada asserção aceita é mantido até expirar, de modo que a mesma resposta enviada duas vezes é recusada. O cache é thread safe e fica em memória. Quando vários servidores compartilham o login, sobrescreva
DoAddToReplayCachepara manter os IDs em um repositório compartilhado. - SHA-1 desativado por padrão. Assinaturas RSA-SHA1 e digests SHA-1 são recusados a menos que você defina
AllowSHA1para um IdP que ainda precise deles.
O parser também recusa declarações DOCTYPE, portanto não há entidades externas, e limita o tamanho e a profundidade de aninhamento do documento. O issuer precisa ser o IdP que você configurou. A primeira verificação que falhar interrompe a validação, e seu motivo fica em ErrorMessage: registre-o em log, e mostre ao usuário uma página simples de “falha ao entrar”.
Teste sem ter conta
Você não precisa de um tenant Entra ID para ver o SAML funcionando. Mock SAML é um identity provider de teste gratuito em mocksaml.com. Ele aceita qualquer service provider e pega a audience e a URL de ACS do AuthnRequest, então não há nada para registrar.
A demo Demos\26.Authentication\03.SAML_ServiceProvider é um service provider completo sobre um TsgcWebSocketHTTPServer, com os endpoints /login, /acs e /metadata em http://localhost:8090:
- Compile a demo e mantenha libcrypto-3.dll e libssl-3.dll junto ao executável. Elas estão na pasta da demo, e o OpenSSL verifica as assinaturas RSA.
- Clique em Load IdP metadata. A fonte padrão é a URL de metadados do mocksaml.com.
- Clique em Start, depois em Open Browser, e siga o link de login.
- No mocksaml.com, digite qualquer nome de usuário do domínio example.com e qualquer senha.
- O navegador volta para o ACS, e a página mostra o NameID, o SessionIndex e os atributos id, email, firstName e lastName.
Quando isso funcionar, abra http://localhost:8090/metadata, registre-o no seu IdP real, carregue os metadados do IdP na demo e faça login novamente. Para AD FS, execute a demo primeiro com SSL, porque o AD FS só aceita https.
Limites atuais
- Sem asserções criptografadas. Uma resposta com um EncryptedAssertion ou um NameID criptografado é recusada. Deixe a criptografia de asserções desativada no IdP. A asserção continua assinada e trafega por https.
- Sem Single Logout. O SLO não está implementado.
SessionIndexé retornado para que sua aplicação possa encerrar sua própria sessão e construir seu próprio logout.
Documentação
Onde conseguir
TsgcSAMLServiceProvider está incluído nas edições Enterprise e All-Access do sgcWebSockets, para Delphi e C++ Builder, e o mesmo componente faz parte do sgcWebSockets .NET. Se você só precisa de autenticação, o pacote sgcAuth o oferece junto com os demais componentes de login. A unit é sgcAuth_SAML_SP, e nada muda em uma aplicação existente até você colocar o componente em um formulário.
Leia também
- Login em Delphi com Passkeys, SAML SSO, LDAP e TOTP 2FA
- PKCE OAuth2 para Delphi
- Autorização com PassKeys
Assista ao vídeo
Há um vídeo curto, “SAML single sign-on in Delphi with Entra ID, Okta and AD FS”, no canal da eSeGeCe. Ele mostra o código na IDE e um login ao vivo com a demo contra o mocksaml.com.
Perguntas, feedback ou ajuda para conectar seu identity provider? Fale conosco. Você receberá uma resposta das pessoas que escreveram o código.
