feat: support bi-directional backchannel SAML SLO

This commit is contained in:
Ali BARIN
2024-05-03 08:28:53 +00:00
parent 40d0fe0db6
commit 3da5e13ecd
6 changed files with 137 additions and 14 deletions

View File

@@ -12,6 +12,7 @@ class AccessToken extends Base {
id: { type: 'string', format: 'uuid' },
userId: { type: 'string', format: 'uuid' },
token: { type: 'string', minLength: 32 },
samlSessionId: { type: ['string', 'null'] },
expiresIn: { type: 'integer' },
revokedAt: { type: ['string', 'null'], format: 'date-time' },
},
@@ -28,8 +29,37 @@ class AccessToken extends Base {
},
});
async terminateRemoteSamlSession() {
if (!this.samlSessionId) {
return;
}
const user = await this
.$relatedQuery('user');
const firstIdentity = await user
.$relatedQuery('identities')
.first();
const samlAuthProvider = await firstIdentity
.$relatedQuery('samlAuthProvider')
.throwIfNotFound();
const response = await samlAuthProvider.terminateRemoteSession(this.samlSessionId);
return response;
}
async revoke() {
return await this.$query().patch({ revokedAt: new Date().toISOString() });
const response = await this.$query().patch({ revokedAt: new Date().toISOString() });
try {
await this.terminateRemoteSamlSession();
} catch (error) {
// TODO: should it silently fail or not?
}
return response;
}
}

View File

@@ -1,5 +1,7 @@
import { URL } from 'node:url';
import { v4 as uuidv4 } from 'uuid';
import appConfig from '../config/app.js';
import axios from '../helpers/axios-with-proxy.js';
import Base from './base.js';
import Identity from './identity.ee.js';
import SamlAuthProvidersRoleMapping from './saml-auth-providers-role-mapping.ee.js';
@@ -61,27 +63,71 @@ class SamlAuthProvider extends Base {
});
static get virtualAttributes() {
return ['loginUrl'];
return ['loginUrl', 'remoteLogoutUrl'];
}
get loginUrl() {
return new URL(`/login/saml/${this.issuer}`, appConfig.baseUrl).toString();
}
get config() {
const callbackUrl = new URL(
get loginCallBackUrl() {
return new URL(
`/login/saml/${this.issuer}/callback`,
appConfig.baseUrl
).toString();
}
get remoteLogoutUrl() {
return this.entryPoint;
}
get config() {
return {
callbackUrl,
callbackUrl: this.loginCallBackUrl,
cert: this.certificate,
entryPoint: this.entryPoint,
issuer: this.issuer,
signatureAlgorithm: this.signatureAlgorithm,
logoutUrl: this.remoteLogoutUrl
};
}
generateLogoutRequestBody(sessionId) {
const logoutRequest = `
<samlp:LogoutRequest
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
ID="${uuidv4()}"
Version="2.0"
IssueInstant="${new Date().toISOString()}"
Destination="${this.remoteLogoutUrl}">
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">${this.issuer}</saml:Issuer>
<samlp:SessionIndex>${sessionId}</samlp:SessionIndex>
</samlp:LogoutRequest>
`;
const encodedLogoutRequest = Buffer.from(logoutRequest).toString('base64')
return encodedLogoutRequest
}
async terminateRemoteSession(sessionId) {
const logoutRequest = this.generateLogoutRequestBody(sessionId);
const response = await axios.post(
this.remoteLogoutUrl,
new URLSearchParams({
SAMLRequest: logoutRequest,
}).toString(),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
}
}
);
return response;
}
}
export default SamlAuthProvider;