From be608dc76c7217c62152e869ab17c9b237a4e99a Mon Sep 17 00:00:00 2001 From: Ludovic Marcotte Date: Wed, 26 Nov 2014 15:09:30 -0500 Subject: [PATCH] Bug fixes for #2378 and #2377 and documentation improvements --- Documentation/SOGoInstallationGuide.asciidoc | 37 ++++++++++-- NEWS | 7 ++- SoObjects/SOGo/SOGoSAML2Metadata.xml | 27 +++++++++ SoObjects/SOGo/SOGoSAML2Session.h | 4 +- SoObjects/SOGo/SOGoSAML2Session.m | 43 ++++++++++++-- UI/MainUI/SOGoSAML2Actions.m | 62 +++++++++++++++++++- UI/MainUI/product.plist | 5 ++ 7 files changed, 170 insertions(+), 15 deletions(-) create mode 100644 SoObjects/SOGo/SOGoSAML2Metadata.xml diff --git a/Documentation/SOGoInstallationGuide.asciidoc b/Documentation/SOGoInstallationGuide.asciidoc index 757055903..728c3d9bf 100644 --- a/Documentation/SOGoInstallationGuide.asciidoc +++ b/Documentation/SOGoInstallationGuide.asciidoc @@ -425,25 +425,31 @@ Defaults to `YES` when unset. |The location of the SSL private key file on the filesystem that is used by SOGo to sign and encrypt communications with the SAML2 identity provider. This file must be generated for each running SOGo service -(rather than host). +(rather than host). Make sure this file is readable by the SOGo user. |S |SOGoSAML2CertiticateLocation |The location of the SSL certificate file. This file must be generated -for each running SOGo service. +for each running SOGo service. Make sure this file is readable by the SOGo user. |S |SOGoSAML2IdpMetadataLocation |The location of the metadata file that describes the services available -on the SAML2 identify provider. +on the SAML2 identify provider. The content of this file is usually generated +directly by your SAML 2.0 IdP solution. For example, using SimpleSAMLphp, you +can get the metadata directly from https://MYSERVER/simplesaml/saml2/idp/metadata.php +Make sure this file is readable by the SOGo user. |S |SOGoSAML2IdpPublicKeyLocation |The location of the SSL public key file on the filesystem that is used by SOGo to sign and encrypt communications with the SAML2 identity provider. This file should be part of the setup of your identity -provider. +provider. Make sure this file is readable by the SOGo user. |S |SOGoSAML2IdpCertificateLocation |The location of the SSL certificate file. This file should be part of -the setup of your identity provider. +the setup of your identity provider. Make sure this file is readable by the SOGo user. + +|S |SOGoSAML2LoginAttribute +|The attribute provided by the IdP to identify the user in SOGo. |S |SOGoSAML2LogoutEnabled |Boolean value indicated whether the "Logout" link is enabled when using @@ -1230,13 +1236,32 @@ documentation of your identity provider and the SAML2 configuration keys that are listed above for proper setup. Once a SOGo instance is configured properly, the metadata for that instance can be retrieved from `http:///SOGo/saml2-metadata` for registration with the -identity provider. +identity provider. SOGo will dynamically generate the metadata based on +the SOGoSAML2CertificateLocation's content and the SOGo server name. + +When using SimpleSAMLphp, make sure the convert OID to names by modifying your +`metadata/saml20-idp-hosted.php` to contain something like this: + +---- + 'attributes.NameFormat' => 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri', + 'authproc' => array( + 100 => array('class' => 'core:AttributeMap', 'oid2name'), + ), +---- + +If you want to test the IdP-initiated logout using SimpleSAMLphp, you can do so by opening +the following URL: + +---- +https://idp.example.org/simplesaml/saml2/idp/SingleLogoutService.php?ReturnTo=www.sogo.nu +---- In order to relay authentication information to your IMAP server and if you make use of the CrudeSAML SASL plugin, you need to make sure that _NGImap4AuthMechanism_ is configured to use the `SAML` mechanism. If you make use of the CrudeSAML PAM plugin, this value may be left empty. + Database Configuration ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/NEWS b/NEWS index dd4869758..2ee8838af 100644 --- a/NEWS +++ b/NEWS @@ -1,8 +1,13 @@ 2.2.11 (2014-xx-xx) ------------------- +Enhancements + - Improved the SAML2 documentation + Bug fixes - - Now possible to specify the username attribute for SAML2 (SOGoSAML2LoginAttribute) + - Now possible to specify the username attribute for SAML2 (SOGoSAML2LoginAttribute) (#2381) + - Added support for IdP-initiated SAML2 logout (#2377) + - We now generate SAML2 metadata on the fly (#2378) 2.2.10 (2014-11-21) ------------------- diff --git a/SoObjects/SOGo/SOGoSAML2Metadata.xml b/SoObjects/SOGo/SOGoSAML2Metadata.xml new file mode 100644 index 000000000..9e4980b47 --- /dev/null +++ b/SoObjects/SOGo/SOGoSAML2Metadata.xml @@ -0,0 +1,27 @@ + + + + %{certificate}%{certificate} + + + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + + + + diff --git a/SoObjects/SOGo/SOGoSAML2Session.h b/SoObjects/SOGo/SOGoSAML2Session.h index de7e086ea..e08217441 100644 --- a/SoObjects/SOGo/SOGoSAML2Session.h +++ b/SoObjects/SOGo/SOGoSAML2Session.h @@ -39,7 +39,9 @@ NSString *assertion; } -+ (NSString *) metadataInContext: (WOContext *) context; ++ (NSString *) metadataInContext: (WOContext *) context + certificate: (NSString *) certificate; + + (NSString *) authenticationURLInContext: (WOContext *) context; + (SOGoSAML2Session *) SAML2SessionInContext: (WOContext *) context; diff --git a/SoObjects/SOGo/SOGoSAML2Session.m b/SoObjects/SOGo/SOGoSAML2Session.m index 0f947d565..261b3c057 100644 --- a/SoObjects/SOGo/SOGoSAML2Session.m +++ b/SoObjects/SOGo/SOGoSAML2Session.m @@ -46,6 +46,28 @@ #import "SOGoSAML2Session.h" +@interface NSString (SOGoCertificateExtension) + +- (NSString *) cleanedUpCertificate; + +@end + +@implementation NSString (SOGoCertificateExtension) + +- (NSString *) cleanedUpCertificate +{ + NSMutableArray *a; + + a = [NSMutableArray arrayWithArray: [self componentsSeparatedByString: @"\n"]]; + [a removeObjectAtIndex: 0]; + [a removeLastObject]; + [a removeLastObject]; + + return [a componentsJoinedByString: @""]; +} + +@end + @interface WOContext (SOGoSAML2Extension) - (NSString *) SAML2ServerURLString; @@ -117,7 +139,9 @@ LassoServerInContext (WOContext *context) format: @"certificate file '%@' could not be read", filename]; - metadata = [SOGoSAML2Session metadataInContext: context]; + metadata = [SOGoSAML2Session metadataInContext: context + certificate: certContent]; + /* FIXME: enable key password in config ? */ server = lasso_server_new_from_buffers ([metadata UTF8String], [keyContent UTF8String], @@ -179,8 +203,10 @@ LassoServerInContext (WOContext *context) } + (NSString *) metadataInContext: (WOContext *) context + certificate: (NSString *) certificate { - NSString *metadata, *serverURLString, *filename; + NSString *serverURLString, *filename; + NSMutableString *metadata; NSBundle *bundle; bundle = [NSBundle bundleForClass: self]; @@ -188,9 +214,16 @@ LassoServerInContext (WOContext *context) if (filename) { serverURLString = [context SAML2ServerURLString]; - metadata = [[NSString stringWithContentsOfFile: filename] - stringByReplacingString: @"%{base_url}" - withString: serverURLString]; + + metadata = [NSMutableString stringWithContentsOfFile: filename]; + [metadata replaceOccurrencesOfString: @"%{base_url}" + withString: serverURLString + options: 0 + range: NSMakeRange(0, [metadata length])]; + [metadata replaceOccurrencesOfString: @"%{certificate}" + withString: [certificate cleanedUpCertificate] + options: 0 + range: NSMakeRange(0, [metadata length])]; } else metadata = nil; diff --git a/UI/MainUI/SOGoSAML2Actions.m b/UI/MainUI/SOGoSAML2Actions.m index 53ab5d1db..834213493 100644 --- a/UI/MainUI/SOGoSAML2Actions.m +++ b/UI/MainUI/SOGoSAML2Actions.m @@ -33,6 +33,8 @@ #import #import +#import +#import #import @interface SOGoSAML2Actions : WODirectAction @@ -42,20 +44,76 @@ - (WOResponse *) saml2MetadataAction { + NSString *metadata, *certContent; + SOGoSystemDefaults *sd; WOResponse *response; - NSString *metadata; response = [context response]; [response setHeader: @"application/xml; charset=utf-8" forKey: @"content-type"]; - metadata = [SOGoSAML2Session metadataInContext: context]; + sd = [SOGoSystemDefaults sharedSystemDefaults]; + + certContent = [NSString stringWithContentsOfFile: [sd SAML2CertificateLocation]]; + + metadata = [SOGoSAML2Session metadataInContext: context + certificate: certContent]; + [response setContentEncoding: NSUTF8StringEncoding]; [response appendContentString: metadata]; return response; } +- (WOResponse *) saml2SingleLogoutServiceAction +{ + NSString *userName, *value, *cookieName; + SOGoWebAuthenticator *auth; + WOResponse *response; + NSCalendarDate *date; + WOCookie *cookie; + NSArray *creds; + + userName = [[context activeUser] login]; + [self logWithFormat: @"SAML2 IdP-initiated SLO for user '%@'", userName]; + + response = [context response]; + + if ([userName isEqualToString: @"anonymous"]) + return response; + + cookie = nil; + + date = [NSCalendarDate calendarDate]; + [date setTimeZone: [NSTimeZone timeZoneWithAbbreviation: @"GMT"]]; + + // We cleanup the memecached/database session cache. We do this before + // invoking _logoutCookieWithDate: in order to obtain its value. + auth = [[SoApplication application] authenticatorInContext: context]; + + if ([auth respondsToSelector: @selector (cookieNameInContext:)]) + { + cookieName = [auth cookieNameInContext: context]; + value = [[context request] cookieValueForKey: cookieName]; + creds = [auth parseCredentials: value]; + + if ([creds count] > 1) + [SOGoSession deleteValueForSessionKey: [creds objectAtIndex: 1]]; + + if ([cookieName length]) + { + cookie = [WOCookie cookieWithName: cookieName value: @"discard"]; + [cookie setPath: [NSString stringWithFormat: @"/%@/", [[context request] applicationName]]]; + [cookie setExpires: [date yesterday]]; + } + } + + if (cookie) + [response addCookie: cookie]; + + return response; +} + - (WOCookie *) _authLocationResetCookieWithName: (NSString *) cookieName { WOCookie *locationCookie; diff --git a/UI/MainUI/product.plist b/UI/MainUI/product.plist index e5c9d2ec1..c9e230177 100644 --- a/UI/MainUI/product.plist +++ b/UI/MainUI/product.plist @@ -133,6 +133,11 @@ actionClass = "SOGoSAML2Actions"; actionName = "saml2SignOnPOST"; }; + saml2-sls = { + protectedBy = ""; + actionClass = "SOGoSAML2Actions"; + actionName = "saml2SingleLogoutService"; + }; /* saml2-signon-redirect = { protectedBy = ""; actionClass = "SOGoSAML2Actions";