Skip to content

Commit 959117c

Browse files
committed
Implement SAML SOAP logout
1 parent d8ad7d8 commit 959117c

File tree

8 files changed

+669
-32
lines changed

8 files changed

+669
-32
lines changed

packages/issuer-saml/src/issuer.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
AuthnContext,
1818
nameIdFormatToUrn,
1919
extractSamlMessage,
20+
extractSamlFromSoap,
21+
wrapInSoapEnvelope,
2022
buildPostForm,
2123
type HttpMethodType,
2224
type Logger,
@@ -347,6 +349,60 @@ export class SAMLIssuer {
347349
};
348350
}
349351

352+
/**
353+
* Process SOAP LogoutRequest and build SOAP response
354+
* @param soapEnvelope - Raw SOAP envelope XML
355+
* @param sessionId - Session ID for restoring session state
356+
* @returns SOAP response envelope as string
357+
*/
358+
async processSoapLogout(
359+
soapEnvelope: string,
360+
sessionId?: string,
361+
): Promise<string> {
362+
// Extract SAML message from SOAP envelope
363+
const samlMessage = extractSamlFromSoap(soapEnvelope);
364+
if (!samlMessage) {
365+
throw new Error("No SAML message found in SOAP envelope");
366+
}
367+
368+
this.logger.debug(
369+
`SAML Issuer: Processing SOAP logout, message length: ${samlMessage.length}`,
370+
);
371+
372+
const server = this.serverManager.getServer();
373+
const logout = new Logout(server);
374+
375+
// Restore session if available
376+
if (sessionId) {
377+
const samlSession = await this.config.getSAMLSession?.(sessionId);
378+
if (samlSession?._lassoSessionDump) {
379+
logout.session = Session.fromDump(samlSession._lassoSessionDump);
380+
}
381+
}
382+
383+
// Process the logout request with SOAP method
384+
logout.processRequestMsg(samlMessage, HttpMethod.SOAP);
385+
logout.validateRequest();
386+
387+
const providerEntityId = logout.remoteProviderId || "";
388+
this.logger.info(
389+
`SAML Issuer: SOAP LogoutRequest from ${providerEntityId}`,
390+
);
391+
392+
// Build the response
393+
const result = logout.buildResponseMsg();
394+
395+
// Get the SAML response body
396+
const samlResponse = result.responseBody || "";
397+
398+
this.logger.info(
399+
`SAML Issuer: Built SOAP LogoutResponse for ${providerEntityId}`,
400+
);
401+
402+
// Wrap in SOAP envelope
403+
return wrapInSoapEnvelope(samlResponse);
404+
}
405+
350406
/**
351407
* Initiate IdP-initiated logout
352408
*/

packages/issuer-saml/src/router.ts

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -160,36 +160,58 @@ export function createSAMLIssuerRouter(
160160
);
161161

162162
/**
163-
* POST /singleLogoutSOAP - SLO endpoint (SOAP)
163+
* POST /singleLogoutSOAP - SLO endpoint (SOAP binding)
164+
* Expects raw XML SOAP envelope in request body
164165
*/
165166
router.post(
166167
"/singleLogoutSOAP",
167-
async (req: Request, res: Response, next: NextFunction) => {
168+
express.text({ type: ["text/xml", "application/soap+xml"] }),
169+
async (req: SAMLRequest, res: Response, _next: NextFunction) => {
168170
try {
169-
// SOAP binding handling
170-
const soapBody = req.body;
171+
// Get raw SOAP envelope from body
172+
const soapEnvelope =
173+
typeof req.body === "string" ? req.body : String(req.body);
171174

172-
// Extract SAML message from SOAP envelope
173-
// This is a simplified implementation
174-
const context = await issuer.processLogoutRequest({
175-
method: "POST",
176-
body: { SAMLRequest: soapBody },
177-
});
178-
179-
const response = await issuer.buildLogoutResponse(context);
180-
181-
// Wrap response in SOAP envelope
182-
const soapResponse = `<?xml version="1.0" encoding="UTF-8"?>
175+
if (!soapEnvelope || soapEnvelope.length === 0) {
176+
res.status(400).set("Content-Type", "text/xml").send(`<?xml version="1.0" encoding="UTF-8"?>
183177
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
184178
<soap:Body>
185-
${response.formData?.SAMLResponse || ""}
179+
<soap:Fault>
180+
<faultcode>soap:Client</faultcode>
181+
<faultstring>Empty SOAP body</faultstring>
182+
</soap:Fault>
186183
</soap:Body>
187-
</soap:Envelope>`;
184+
</soap:Envelope>`);
185+
return;
186+
}
187+
188+
// Get session ID if available
189+
const sessionId = config.getSessionId?.(req) || undefined;
190+
191+
// Process SOAP logout and get SOAP response
192+
const soapResponse = await issuer.processSoapLogout(
193+
soapEnvelope,
194+
sessionId,
195+
);
188196

189197
res.set("Content-Type", "text/xml");
190198
res.send(soapResponse);
191199
} catch (err) {
192-
next(err);
200+
// Return SOAP fault on error
201+
const errorMessage =
202+
err instanceof Error ? err.message : "Unknown error";
203+
res
204+
.status(500)
205+
.set("Content-Type", "text/xml")
206+
.send(`<?xml version="1.0" encoding="UTF-8"?>
207+
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
208+
<soap:Body>
209+
<soap:Fault>
210+
<faultcode>soap:Server</faultcode>
211+
<faultstring>${errorMessage.replace(/[<>&"']/g, (c) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;", '"': "&quot;", "'": "&apos;" })[c] || c)}</faultstring>
212+
</soap:Fault>
213+
</soap:Body>
214+
</soap:Envelope>`);
193215
}
194216
},
195217
);

packages/lib-saml/src/index.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ export {
3131
escapeXml,
3232
buildUrl,
3333
parseQueryString,
34+
// SOAP utilities
35+
extractSamlFromSoap,
36+
wrapInSoapEnvelope,
37+
isSoapEnvelope,
3438
} from "./utils";
3539

3640
// Export lasso loader utilities
@@ -44,6 +48,11 @@ export {
4448
SignatureMethod,
4549
NameIdFormat,
4650
AuthnContext,
51+
// Runtime class wrappers
52+
Login,
53+
Logout,
54+
Identity,
55+
Session,
4756
} from "./lasso-loader";
4857

4958
// Re-export lasso types with full names
@@ -56,9 +65,3 @@ export type {
5665
HttpMethodType,
5766
SignatureMethodType,
5867
} from "./lasso-loader";
59-
60-
// Backward-compatible type aliases (without Lasso prefix)
61-
export type { LassoLogin as Login } from "./lasso-loader";
62-
export type { LassoLogout as Logout } from "./lasso-loader";
63-
export type { LassoIdentity as Identity } from "./lasso-loader";
64-
export type { LassoSession as Session } from "./lasso-loader";

0 commit comments

Comments
 (0)