MattermostSamlIntegration.java
package com.cloudforge.core.oidc;
import com.cloudforge.core.interfaces.Ec2Context;
import com.cloudforge.core.interfaces.OidcConfiguration;
import com.cloudforge.core.interfaces.OidcIntegration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* SAML 2.0 integration for Mattermost.
*
* <p><strong>Why SAML over OIDC:</strong></p>
* <ul>
* <li>SAML supports group synchronization via AD/LDAP integration</li>
* <li>OIDC in Mattermost does NOT support LDAP data sync</li>
* <li>SAML enables automatic team/channel membership management</li>
* <li>Role mapping from IdP groups to Mattermost roles</li>
* </ul>
*
* <p><strong>AWS Cognito as SAML IdP:</strong></p>
* <ul>
* <li>Configure Cognito User Pool as SAML 2.0 Identity Provider</li>
* <li>Set ACS URL to: https://{mattermost}/login/sso/saml</li>
* <li>Map attributes: email, firstName, lastName, groups</li>
* </ul>
*
* <p><strong>IAM Identity Center as SAML IdP:</strong></p>
* <ul>
* <li>Create custom SAML 2.0 application in Identity Center</li>
* <li>Configure attribute mappings for user attributes</li>
* <li>Assign users and groups to the application</li>
* </ul>
*
* <p><strong>Group Sync (Enterprise):</strong></p>
* <p>With SAML + AD/LDAP sync enabled, Mattermost can:</p>
* <ul>
* <li>Automatically add users to teams based on group membership</li>
* <li>Manage channel membership via groups</li>
* <li>Assign admin roles based on group membership</li>
* </ul>
*
* @see <a href="https://docs.mattermost.com/onboard/sso-saml.html">Mattermost SAML Documentation</a>
* @see <a href="https://docs.mattermost.com/onboard/sso-saml-ldapsync.html">SAML + AD/LDAP Sync</a>
*/
public class MattermostSamlIntegration implements OidcIntegration {
/**
* Mount path for the SAML certificate directory.
* <p><strong>Important:</strong> Do NOT use /mattermost/config because
* Mattermost needs write access to /mattermost/config/config.json.</p>
*/
private static final String SAML_MOUNT_PATH = "/mattermost/saml";
/**
* Full path to the SAML IdP certificate file.
* @deprecated Use {@link #getSamlCertificateFilePath()} instead
*/
@Deprecated
public static final String SAML_CERTIFICATE_MOUNT_PATH = SAML_MOUNT_PATH + "/idp.crt";
@Override
public boolean isSupported() {
return true;
}
@Override
public String getIntegrationMethod() {
return "SAML 2.0 authentication (configured via MM_SAMLSETTINGS_* environment variables). " +
"Supports group sync via AD/LDAP integration for automatic team/channel membership.";
}
@Override
public Map<String, String> getEnvironmentVariables(OidcConfiguration config) {
// Check provider type - delegate to OIDC for "cognito", use SAML for "cognito-saml" or "identity-center"
if ("cognito".equals(config.getProviderType())) {
// Delegate to OIDC integration
MattermostOidcIntegration oidcIntegration = new MattermostOidcIntegration();
return oidcIntegration.getEnvironmentVariables(config);
}
Map<String, String> env = new HashMap<>();
// Enable SAML login
env.put("MM_SAMLSETTINGS_ENABLE", "true");
// Identity Provider configuration
String idpUrl = getSamlSsoUrl(config);
env.put("MM_SAMLSETTINGS_IDPURL", idpUrl);
// IdP Issuer/Entity ID
env.put("MM_SAMLSETTINGS_IDPDESCRIPTORURL", config.getIssuerUrl());
// IdP Metadata URL - stored for reference, but Mattermost REQUIRES the actual certificate file
// The metadata URL is used by ContainerFactory's init container to fetch the certificate
String metadataUrl = getSamlMetadataUrl(config);
if (metadataUrl != null) {
env.put("MM_SAMLSETTINGS_IDPMETADATAURL", metadataUrl);
}
// Service Provider configuration
// Use applicationUrl if available, otherwise derive from redirectUrl
String siteUrl = getEffectiveSiteUrl(config);
env.put("MM_SERVICESETTINGS_SITEURL", siteUrl);
// Service Provider Identifier (Entity ID) - REQUIRED
// This is how Mattermost identifies itself to the IdP
env.put("MM_SAMLSETTINGS_SERVICEPROVIDERIDENTIFIER", siteUrl);
// Mattermost SAML callback URL (ACS URL)
env.put("MM_SAMLSETTINGS_ASSERTIONCONSUMERSERVICEURL", siteUrl + "/login/sso/saml");
// IdP certificate file path - ALWAYS required by Mattermost
// For ECS Fargate deployments, ContainerFactory creates an init container that:
// 1. Fetches the SAML metadata XML from the metadata URL
// 2. Extracts the X509Certificate from the XML
// 3. Writes it to this path in a shared volume
// The main container then mounts this volume and reads the certificate
env.put("MM_SAMLSETTINGS_IDPCERTIFICATEFILE", SAML_CERTIFICATE_MOUNT_PATH);
// Signing configuration
env.put("MM_SAMLSETTINGS_VERIFY", "true");
env.put("MM_SAMLSETTINGS_ENCRYPT", "false");
env.put("MM_SAMLSETTINGS_SIGNREQUEST", "false");
// Attribute mappings - standard SAML attributes
env.put("MM_SAMLSETTINGS_EMAILATTRIBUTE", "email");
env.put("MM_SAMLSETTINGS_USERNAMEATTRIBUTE", getUsernameAttribute(config));
env.put("MM_SAMLSETTINGS_FIRSTNAMEATTRIBUTE", "firstName");
env.put("MM_SAMLSETTINGS_LASTNAMEATTRIBUTE", "lastName");
// Optional attributes
env.put("MM_SAMLSETTINGS_NICKNAMEATTRIBUTE", "");
env.put("MM_SAMLSETTINGS_POSITIONATTRIBUTE", "");
env.put("MM_SAMLSETTINGS_LOCALEATTRIBUTE", "");
// Admin attribute for automatic admin role assignment
// Must enable EnableAdminAttribute first, then set the attribute condition
// Format: "field=value" - users with matching SAML attribute become System Admins
// Disabled by default - enable when IdP sends admin role information
env.put("MM_SAMLSETTINGS_ENABLEADMINATTRIBUTE", "false");
env.put("MM_SAMLSETTINGS_ADMINATTRIBUTE", "");
// Guest attribute for automatic guest role assignment
// Format: "field=value" - users with matching SAML attribute become Guests
// Disabled by default - enable when IdP sends guest role information
env.put("MM_SAMLSETTINGS_GUESTATTRIBUTE", "");
// Login button customization
String providerType = config.getProviderType();
String buttonText;
if ("cognito".equals(providerType) || "cognito-saml".equals(providerType)) {
buttonText = "Sign in with AWS Cognito";
} else {
buttonText = "Sign in with AWS IAM Identity Center";
}
env.put("MM_SAMLSETTINGS_LOGINBUTTONTEXT", buttonText);
// Enable AD/LDAP sync with SAML (Enterprise feature)
// This allows group-based team/channel management
env.put("MM_SAMLSETTINGS_ENABLESYNCWITHLDAP", "true");
return env;
}
@Override
public List<String> getUserDataCommands(OidcConfiguration config, Ec2Context context) {
List<String> commands = new ArrayList<>();
commands.add("# Configure Mattermost SAML 2.0 integration");
commands.add("# SAML provides group sync support that OIDC lacks");
commands.add("");
// Create certificate directory
commands.add("# Create certificate directory for SAML");
commands.add("mkdir -p /opt/mattermost/config/saml");
commands.add("chown -R 2000:2000 /opt/mattermost/config/saml");
commands.add("");
// Retrieve SAML IdP certificate from Secrets Manager
commands.add("# Retrieve SAML IdP certificate from AWS Secrets Manager");
commands.add("echo 'Retrieving SAML IdP certificate...' >> /var/log/userdata.log");
commands.add("SAML_CERT=$(aws secretsmanager get-secret-value \\");
commands.add(" --secret-id " + config.getClientSecretArn() + " \\");
commands.add(" --query SecretString --output text 2>>/var/log/userdata.log)");
commands.add("");
commands.add("if [ -z \"$SAML_CERT\" ]; then");
commands.add(" echo 'ERROR: Failed to retrieve SAML IdP certificate' >> /var/log/userdata.log");
commands.add(" export SAML_RETRIEVAL_FAILED=true");
commands.add("else");
commands.add(" echo \"$SAML_CERT\" > /opt/mattermost/config/saml/idp-certificate.crt");
commands.add(" chmod 644 /opt/mattermost/config/saml/idp-certificate.crt");
commands.add(" chown 2000:2000 /opt/mattermost/config/saml/idp-certificate.crt");
commands.add(" echo 'SAML IdP certificate saved' >> /var/log/userdata.log");
commands.add("fi");
commands.add("");
// Create SAML environment file
commands.add("# Create Mattermost SAML configuration");
commands.add("cat > /opt/mattermost/mattermost-saml-env.sh <<'EOF'");
commands.add("export MM_SAMLSETTINGS_ENABLE=true");
String idpUrl = getSamlSsoUrl(config);
commands.add("export MM_SAMLSETTINGS_IDPURL=\"" + idpUrl + "\"");
commands.add("export MM_SAMLSETTINGS_IDPDESCRIPTORURL=\"" + config.getIssuerUrl() + "\"");
String metadataUrl = getSamlMetadataUrl(config);
if (metadataUrl != null) {
commands.add("export MM_SAMLSETTINGS_IDPMETADATAURL=\"" + metadataUrl + "\"");
}
// Service provider config - use applicationUrl or derive from redirectUrl
String siteUrl = getEffectiveSiteUrl(config);
commands.add("export MM_SERVICESETTINGS_SITEURL=\"" + siteUrl + "\"");
// Service Provider Identifier (Entity ID) - REQUIRED
commands.add("export MM_SAMLSETTINGS_SERVICEPROVIDERIDENTIFIER=\"" + siteUrl + "\"");
commands.add("export MM_SAMLSETTINGS_ASSERTIONCONSUMERSERVICEURL=\"" + siteUrl + "/login/sso/saml\"");
// Certificate path
commands.add("export MM_SAMLSETTINGS_IDPCERTIFICATEFILE=\"/mattermost/config/saml/idp-certificate.crt\"");
// Security settings
commands.add("export MM_SAMLSETTINGS_VERIFY=true");
commands.add("export MM_SAMLSETTINGS_ENCRYPT=false");
commands.add("export MM_SAMLSETTINGS_SIGNREQUEST=false");
// Attribute mappings
commands.add("export MM_SAMLSETTINGS_EMAILATTRIBUTE=\"email\"");
commands.add("export MM_SAMLSETTINGS_USERNAMEATTRIBUTE=\"" + getUsernameAttribute(config) + "\"");
commands.add("export MM_SAMLSETTINGS_FIRSTNAMEATTRIBUTE=\"firstName\"");
commands.add("export MM_SAMLSETTINGS_LASTNAMEATTRIBUTE=\"lastName\"");
// Role attributes - disabled by default (empty = disabled)
// To enable: set EnableAdminAttribute=true and AdminAttribute="field=value"
commands.add("export MM_SAMLSETTINGS_ENABLEADMINATTRIBUTE=false");
commands.add("export MM_SAMLSETTINGS_ADMINATTRIBUTE=\"\"");
commands.add("export MM_SAMLSETTINGS_GUESTATTRIBUTE=\"\"");
// Button text
String providerType = config.getProviderType();
String buttonText = ("cognito".equals(providerType) || "cognito-saml".equals(providerType)) ?
"Sign in with AWS Cognito" : "Sign in with AWS IAM Identity Center";
commands.add("export MM_SAMLSETTINGS_LOGINBUTTONTEXT=\"" + buttonText + "\"");
// Enable LDAP sync
commands.add("export MM_SAMLSETTINGS_ENABLESYNCWITHLDAP=true");
commands.add("EOF");
commands.add("");
// Verify certificate was retrieved
commands.add("if [ \"$SAML_RETRIEVAL_FAILED\" = \"true\" ]; then");
commands.add(" echo 'SAML configuration incomplete - certificate missing' >> /var/log/userdata.log");
commands.add(" exit 1");
commands.add("fi");
commands.add("");
commands.add("# Source environment for container startup");
commands.add("chmod 600 /opt/mattermost/mattermost-saml-env.sh");
commands.add("source /opt/mattermost/mattermost-saml-env.sh");
commands.add("");
commands.add("echo 'Mattermost SAML integration configured' >> /var/log/userdata.log");
commands.add("echo 'Group sync available via AD/LDAP integration (Enterprise)' >> /var/log/userdata.log");
return commands;
}
@Override
public String getContainerStartupCommand() {
// Mattermost uses a Go binary, not a shell script
// The official image is distroless (no /bin/sh)
return "/mattermost/bin/mattermost";
}
@Override
public boolean isDistroless() {
// Mattermost official image is distroless - contains only Go binary
// No /bin/sh available, must use environment variables only
return true;
}
@Override
public boolean supportsCognito() {
// Cognito requires manual SAML setup in Cognito console
// Not auto-provisioned, but works if user configures it
return true;
}
@Override
public boolean supportsIdentityCenterSaml() {
// Full support - auto-provisioned via IdentityCenterSamlFactory
return true;
}
@Override
public String getAuthenticationType() {
return "SAML";
}
@Override
public String getPostDeploymentInstructions() {
return """
Mattermost SAML 2.0 Integration (Enterprise Edition)
====================================================
EDITION INFO:
This deployment uses Mattermost Enterprise Edition which runs
in free mode by default. SAML authentication works without a
license, but group sync requires an Enterprise license.
To activate Enterprise features:
1. Go to System Console > Edition and License
2. Upload license file or start a 30-day trial
3. Enterprise features unlock immediately
AWS Cognito Setup:
1. Enable SAML 2.0 in Cognito User Pool (App Integration)
2. Download SAML metadata/certificate from Cognito
3. Store certificate in AWS Secrets Manager
4. Configure attribute mappings in Cognito:
- email -> email
- given_name -> firstName
- family_name -> lastName
IAM Identity Center Setup:
1. Create custom SAML 2.0 application
2. Set ACS URL: https://{your-mattermost}/login/sso/saml
3. Set Entity ID: https://{your-mattermost}
4. Map attributes: email, firstName, lastName
5. Assign users/groups to the application
Mattermost Configuration:
1. Access: https://{your-domain}:8065
2. System Console > Authentication > SAML 2.0
3. Verify settings match your IdP configuration
4. Test SSO login
Group Sync (requires Enterprise license + AD/LDAP):
Group sync requires both an Enterprise license AND a separate
AD/LDAP server (e.g., AWS Managed Microsoft AD).
1. System Console > Authentication > AD/LDAP
2. Configure AD/LDAP connection
3. Enable "Enable AD/LDAP Synchronization with SAML"
4. Configure group-to-team/channel mappings
""";
}
/**
* Gets the effective site URL for Mattermost.
*
* <p>Priority order:</p>
* <ol>
* <li>applicationUrl if configured (custom domain like https://mattermost.example.com)</li>
* <li>Derived from redirectUrl (extract base URL from callback URL)</li>
* </ol>
*/
private String getEffectiveSiteUrl(OidcConfiguration config) {
// First try applicationUrl (custom domain)
String appUrl = config.getApplicationUrl();
if (appUrl != null && !appUrl.isEmpty()) {
return appUrl;
}
// Fallback: derive from redirectUrl
// redirectUrl is typically: https://alb-dns-name.region.elb.amazonaws.com/login/sso/saml
// or https://mattermost.example.com/login/sso/saml
String redirectUrl = config.getRedirectUrl();
if (redirectUrl != null && !redirectUrl.isEmpty()) {
// Extract base URL (everything before the path)
try {
java.net.URI uri = new java.net.URI(redirectUrl);
String scheme = uri.getScheme();
String host = uri.getHost();
int port = uri.getPort();
if (port > 0 && port != 443 && port != 80) {
return scheme + "://" + host + ":" + port;
} else {
return scheme + "://" + host;
}
} catch (Exception e) {
// If URI parsing fails, return a placeholder
return "https://mattermost.example.com";
}
}
// Last resort fallback
return "https://mattermost.example.com";
}
/**
* Gets the SAML SSO URL for the identity provider.
*/
private String getSamlSsoUrl(OidcConfiguration config) {
String providerType = config.getProviderType();
if ("cognito".equals(providerType) || "cognito-saml".equals(providerType)) {
// Cognito SAML SSO endpoint (via Keycloak for cognito-saml)
return config.getIssuerUrl() + "/saml2/idp/SSO";
} else if ("identity-center".equals(providerType)) {
// Identity Center SAML SSO URL
// If authorizationEndpoint already contains SAML path (set by ApplicationOidcFactory),
// use it directly. Otherwise, derive from OIDC endpoint.
String authEndpoint = config.getAuthorizationEndpoint();
if (authEndpoint != null && authEndpoint.contains("/saml/")) {
return authEndpoint;
}
// Derive SAML URL from OIDC endpoint (for tests using IdentityCenterOidcConfiguration)
return authEndpoint.replace("/oauth2/authorize", "/saml/SSO");
} else {
// External OIDC provider - try to derive SAML URL
return config.getAuthorizationEndpoint().replace("/oauth2/authorize", "/saml/SSO");
}
}
/**
* Gets the SAML metadata URL for automatic IdP configuration.
*/
private String getSamlMetadataUrl(OidcConfiguration config) {
String providerType = config.getProviderType();
if ("cognito".equals(providerType) || "cognito-saml".equals(providerType)) {
// Cognito provides SAML metadata at this URL (via Keycloak for cognito-saml)
return config.getIssuerUrl() + "/saml2/idp/metadata";
} else if ("identity-center".equals(providerType)) {
// Identity Center SAML metadata URL
// If tokenEndpoint already contains SAML path (set by ApplicationOidcFactory),
// use it directly. Otherwise, derive from OIDC endpoint.
String tokenEndpoint = config.getTokenEndpoint();
if (tokenEndpoint != null && tokenEndpoint.contains("/saml/")) {
return tokenEndpoint;
}
// Derive SAML metadata URL from OIDC endpoint
String authEndpoint = config.getAuthorizationEndpoint();
return authEndpoint.replace("/oauth2/authorize", "/saml/metadata");
} else {
// External OIDC provider - try to derive metadata URL
return config.getAuthorizationEndpoint().replace("/oauth2/authorize", "/saml/metadata");
}
}
/**
* Gets the username attribute based on provider type.
*/
private String getUsernameAttribute(OidcConfiguration config) {
if (config.getProviderType().equals("cognito")) {
// Cognito uses preferred_username or custom attribute
return "preferred_username";
} else {
// Identity Center uses preferred_username
return "preferred_username";
}
}
// ==================== SAML Certificate Configuration ====================
@Override
public String getSamlCertificateMountPath() {
return SAML_MOUNT_PATH;
}
@Override
public String getSamlCertificateFilePath() {
return SAML_MOUNT_PATH + "/idp.crt";
}
@Override
public String getSamlCertificateEnvVar() {
return "MM_SAMLSETTINGS_IDPCERTIFICATEFILE";
}
}