1
0
mirror of https://github.com/Alinto/sogo.git synced 2025-04-18 10:04:00 +03:00

feat(openID): second part with a lot of fixes and cleaning

This commit is contained in:
Hivert Quentin 2025-03-13 15:24:53 +01:00
parent 458d39d48a
commit c3234882eb
34 changed files with 1539 additions and 448 deletions

View File

@ -1559,6 +1559,81 @@ SOGoIMAPCASServiceName should be set to the actual imap service name
expected by pam_cas, otherwise it will fail to authenticate incoming
connection properly.
Authenticating using OPENID
~~~~~~~~~~~~~~~~~~~~~~~~~~~
SOGo natively supports OPENID authentication. For activating OpenId authentication you need first to make sure that
_SOGoAuthenticationType_ is set to `openid`,
_SOGoXSRFValidationEnabled_ is set to `NO` and set the following parameters:
[cols="^4,46,50a"]
|=======================================================================
|S |OCSOpenIdURL
|Parameter used to set the database URL for openID session.
For MariaDB/MySQL, set the database URL to something like: mysql://sogo:sogo@127.0.0.1:3306/sogo/sogo_openid.
|S |SOGoOpenIdConfigUrl
|Parameter used to specify the endpoint of OpenID Provider Configuration, mandatory. For example:
https://myopenid.net/.well-known/openid-configuration
|S |SOGoOpenIdClient
|Name of your openid client, mandatory.
|S |SOGoOpenIdClientSecret
|Secret of your openid client, mandatory
|S |SOGoOpenIdScope
|Scope or your openid client, mandatory. List of words space separated like this:
"openid profile email"
|S |SOGoOpenIdEmailParam
|Name of the parameter from user profile that contains the mail/uid.
Defaults to `email` when unset.
|S |SOGoOpenIdEnableRefreshToken
|Set to `YES` to Enable the mechanism of refresh token if provided. You may have to configure
and/or add a value to your scope for it to work.
Defaults to `NO` when unset.
|S |SOGoOpenIdTokenCheckInterval
|Number of seconds before sogo check again the user's access token validity.
This is to prevent sogo to do too much request to the openid server.
Defaults to `0` when unset.
|S |SOGoOpenIdLogoutEnabled
|Allow user to end their openId with the webmail. Meaning that will disconnect them from
the others applicaitons as well.
Defaults to `NO` when unset.
|=======================================================================
The tricky part shows up for the imap and smtp sever. SOGo doesn't know the password
of the user and only have its access token. A new auth mechanism has been implemented,
the https://developers.google.com/gmail/imap/xoauth2-protocol#initial_client_response[xoauth2]
You can set it with the parameter _NGImap4AuthMechanism_ and/or _SOGoSMTPAuthenticationType_
*With dovecot:* +
Dovecot natively supports xoauth2 and can be figured as such: https://doc.dovecot.org/2.3/configuration_manual/authentication/oauth2/
*With cyrus:* +
Cyrus doesn't support xoauth2 mechanism and pluggins or homemade solutions must be found. +
_Please note, as Alinto uses dovecot, we didn't investigate cyrus' case. If one member of the community
finds a solution, we will be happy to update this documentation._
As you can see, a new database table is used for handling openid session. The table is automaticcaly created when _OCSOpenIdURL_ is set.
If the user quits the webmail without logging out or trough another application,
the session will stays in the table and be useless. That's why a new sogo-tool command has been added to clean this table.
You can put it in a cron to do that periodicly. +
See _<<sogo-tool-clean-openid-sessions,sogo-tool clean-openid-sessions>>_.
Authenticating using SAML2
~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -2113,8 +2188,7 @@ To disable TLS verification for localhost domains, add
|D |SOGoSMTPAuthenticationType
|Activate SMTP authentication and specifies which type is in use.
Current, only `PLAIN` is supported and other values will cause
the authentication to fail.
Current, Are supported `PLAIN` and 'xoauth2' for openid.
|D |SOGoSMTPMasterUserEnabled
|Enable specific SMTP user account for system e-mails (notifications, reminders, ...). Default is `NO`.
@ -2297,6 +2371,7 @@ SASL mechanism. Using `AUTHENTICATE` instead of `LOGIN` is also necessary
to enable UTF-8 characters in users' passwords. To enable simple use of
`AUTHENTICATE` for this purpose, set this setting to `plain`. Please note
that this feature might be limited at this time.
Now support `xoauth2` mechanism when using openid. Be sure you imap server undesrtands this mechanism.
|D |NGImap4ConnectionGroupIdPrefix
|Prefix to prepend to names in IMAP ACL transactions, to indicate the
@ -3630,6 +3705,22 @@ sogo-tool checkup user1
sogo-tool checkup -d user1
----
[[sogo-tool-clean-openid-sessions]]
sogo-tool clean-openid-sessions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Obviously only usefull if you have set SOGo with openId authentication.
Will clean all expired openId sessions from the database.
sogo-tool clean-openid-sessions
Example:
----
sogo-tool clean-openid-sessions
----
sogo-tool cleanup
~~~~~~~~~~~~~~~~~

View File

@ -327,17 +327,9 @@ static BOOL debugLeaks;
}
//Create mandatory openId table, if used
if([[defaults authenticationType] isEqualToString: @"openid"])
if([defaults hasOpenIdType])
{
value = [defaults stringForKey: @"OCSOpenIdURL"];
if (value)
[[fm openIdFolder] createFolderIfNotExists];
else
{
[self errorWithFormat: @"No value specified for 'OCSOpenIdURL' for auth mode %@", [defaults authenticationType]];
ok = NO;
}
[[fm openIdFolder] createFolderIfNotExists];
}
}

View File

@ -132,14 +132,24 @@
- (NGImap4ConnectionManager *) mailManager
{
return [NGImap4ConnectionManager defaultConnectionManager];
SOGoSystemDefaults *sd;
NSString *imapAuthMech, *domain;
domain = [[[self context] activeUser] loginDomain];
sd = [SOGoSystemDefaults sharedSystemDefaults];
if([sd doesLoginTypeByDomain])
imapAuthMech = [sd getImapAuthMechForDomain: domain];
else
imapAuthMech = nil;
return [NGImap4ConnectionManager defaultConnectionManager: imapAuthMech];
}
- (NGImap4Connection *) _createIMAP4Connection
{
NGImap4ConnectionManager *manager;
NGImap4Connection *newConnection;
NSString *password;
NSString *password, *domain;
NGInternetSocketAddress *host;
SOGoSystemDefaults *sd;
BOOL usesSSO;
@ -152,12 +162,15 @@
host = [NGInternetSocketAddress addressWithPort:0 onHost:[[self imap4URL] host]];
sd = [SOGoSystemDefaults sharedSystemDefaults];
usesSSO = [sd isSsoUsed];
domain = [[[self context] activeUser] loginDomain];
usesSSO = [sd isSsoUsed: domain];
if (![[[self mailAccountFolder] nameInContainer] isEqualToString: @"0"] &&
usesSSO &&
[host isLocalhost])
{
//
[self errorWithFormat: @"Trying to use localhost for additional IMAP account - aborting."];
return nil;
}
@ -227,19 +240,17 @@
login = [[[self context] activeUser] login];
if (!login)
login = [[[[self container] context] activeUser] login];
login = [[[[self container] context] activeUser] login];
cacheKey = [NSString stringWithFormat: @"%@+%@",
login,
[[self mailAccountFolder] nameInContainer]];
cacheKey = [NSString stringWithFormat: @"%@+%@", login, [[self mailAccountFolder] nameInContainer]];
imap4 = [sogoCache imap4ConnectionForKey: cacheKey];
if (!imap4)
{
imap4 = [self _createIMAP4Connection];
[sogoCache registerIMAP4Connection: imap4
forKey: cacheKey];
}
[imap4 retain];
{
imap4 = [self _createIMAP4Connection];
[sogoCache registerIMAP4Connection: imap4
forKey: cacheKey];
}
[imap4 retain];
}
// Connection broken, try to reconnect

View File

@ -95,6 +95,7 @@
return keysWithFormat;
}
- (NSComparisonResult) caseInsensitiveDisplayNameCompare: (NSDictionary *) theDictionary
{
return [[self objectForKey: @"cn"] caseInsensitiveCompare: [theDictionary objectForKey: @"cn"]];

View File

@ -80,7 +80,10 @@ static int cssEscapingCount;
{
hostR = [self rangeOfString: @"://"];
locationR = [[self substringFromIndex: (hostR.location + hostR.length)] rangeOfString: @"/"];
newURL = [self substringFromIndex: (hostR.location + hostR.length + locationR.location)];
if(locationR.location != NSNotFound)
newURL = [self substringFromIndex: (hostR.location + hostR.length + locationR.location)];
else
newURL = @"";
}
return newURL;

View File

@ -141,6 +141,7 @@
return password;
}
/* create SOGoUser */
- (SOGoUser *) userInContext: (WOContext *)_ctx

View File

@ -62,6 +62,7 @@ extern NSString *SOGoDefaultsSourceUnmutableSource;
- (void) setBool: (BOOL) value forKey: (NSString *) key;
- (BOOL) boolForKey: (NSString *) key;
- (BOOL) boolForKey: (NSString *) key andDict: (NSDictionary*) _dict;
- (void) setFloat: (float) value forKey: (NSString *) key;
- (float) floatForKey: (NSString *) key;

View File

@ -164,6 +164,29 @@ static Class NSStringKlass = Nil;
return value;
}
- (BOOL) boolForKey: (NSString *) key andDict: (NSDictionary*) _dict
{
id boolForKey;
BOOL value;
boolForKey = [_dict objectForKey: key];
if (boolForKey)
{
if ([boolForKey respondsToSelector: @selector (boolValue)])
value = [boolForKey boolValue];
else
{
[self warnWithFormat: @"expected a boolean for '%@' (ignored)",
key];
value = NO;
}
}
else
value = NO;
return value;
}
- (void) setFloat: (float) value
forKey: (NSString *) key
{

View File

@ -25,6 +25,7 @@
@class NSArray;
@class NSException;
@class NSString;
@class NSDictionary;
@class WOContext;
@class SOGoDomainDefaults;

View File

@ -31,6 +31,7 @@
#import "NSString+Utilities.h"
#import "SOGoStaticAuthenticator.h"
#import "SOGoEmptyAuthenticator.h"
#import "SOGoWebAuthenticator.h"
#import "SOGoSystemDefaults.h"
#import "SOGoUser.h"
#import "SOGoUserManager.h"
@ -265,7 +266,7 @@
inContext: (WOContext *) woContext
systemMessage: (BOOL) isSystemMessage
{
NSString *currentTo, *login, *password;
NSString *currentTo, *login, *password, *encryption, *protocol, *server;
NSString * smtpAuthMethod;
NSDictionary *currentAcount;
NSMutableArray *toErrors;
@ -274,6 +275,7 @@
NSException *result;
NSURL * smtpUrl;
SOGoUser* user;
SOGoSystemDefaults *sd;
BOOL doSmtpAuth;
result = nil;
@ -297,6 +299,22 @@
doSmtpAuth = [currentAcount objectForKey: @"smtpAuth"] ? [[currentAcount objectForKey: @"smtpAuth"] boolValue] : NO;
}
sd = [SOGoSystemDefaults sharedSystemDefaults];
if(userId == 0 && [sd doesLoginTypeByDomain] && [authenticator isKindOfClass: [SOGoWebAuthenticator class]])
{
//Check if the authentication depends on the domain, only for webmail requets not for dav...
NSString *username, *_domain;
NSRange r;
username = [currentAcount objectForKey: @"userName"];
r = [username rangeOfString: @"@"];
if (r.location != NSNotFound)
{
_domain = [username substringFromIndex: r.location+1];
smtpAuthMethod = [sd getSmtpAuthMechForDomain: _domain];
}
}
NS_DURING
{
[client connect];
@ -319,7 +337,18 @@
getExternalLoginForUID: [[authenticator userInContext: woContext] loginInDomain]
inDomain: [[authenticator userInContext: woContext] domain]];
password = [authenticator passwordInContext: woContext];
encryption = [currentAcount objectForKey: @"encryption"];
protocol = @"imap";
if ([encryption isEqualToString: @"ssl"] || [encryption isEqualToString: @"tls"])
protocol = @"imaps";
server = [NSString stringWithFormat: @"%@://%@", protocol, [currentAcount objectForKey: @"serverName"]];
if([authenticator isKindOfClass: [SOGoWebAuthenticator class]])
password = [authenticator smtpPasswordInContext: woContext forURL: server];
else
{
password = [authenticator passwordInContext: woContext];
smtpAuthMethod = @"plain"; //is a dav or activesync request, only support plain method
}
}

View File

@ -25,6 +25,7 @@
https://openid.net/developers/how-connect-works/ */
#import <NGObjWeb/WOResponse.h>
#import <SOGo/SOGoObject.h>
@class NSString;
@ -33,7 +34,7 @@
@class NSJSONSerialization;
@interface SOGoOpenIdSession : NSObject
@interface SOGoOpenIdSession : SOGoObject
{
//For cache
BOOL cacheUpdateNeeded;
@ -47,6 +48,10 @@
NSString *openIdClientSecret;
NSString *openIdEmailParam;
BOOL openIdEnableRefreshToken;
BOOL sendDomainInfo;
NSString *forDomain;
//From request to well-known/configuration
NSString *authorizationEndpoint;
@ -66,16 +71,20 @@
}
+ (BOOL) checkUserConfig;
+ (SOGoOpenIdSession *) OpenIdSession;
+ (SOGoOpenIdSession *) OpenIdSession: (NSString *) _domain;
+ (SOGoOpenIdSession *) OpenIdSessionWithConfig: (NSDictionary *) _config;
+ (SOGoOpenIdSession *) OpenIdSessionWithToken: (NSString *) token domain: (NSString *) _domain;
+ (SOGoOpenIdSession *) OpenIdSessionWithTokenAndConfig: (NSString *) token config: (NSDictionary *) _config;
+ (void) deleteValueForSessionKey: (NSString *) theSessionKey;
- (void) initialize;
- (void) initializeWithConfig: (NSDictionary *) _config;
- (BOOL) sessionIsOK;
- (WOResponse *) _performOpenIdRequest: (NSString *) endpoint
method: (NSString *) method
headers: (NSDictionary *) headers
body: (NSData *) body;
- (NSMutableDictionary *) fecthConfiguration;
- (NSMutableDictionary *) fecthConfiguration: (NSString *) _domain;
- (void) setAccessToken;
- (NSString *) getRefreshToken;
- (NSString *) getToken;

View File

@ -23,6 +23,9 @@
#import <NGObjWeb/WOResponse.h>
#import <NGExtensions/NSObject+Logs.h>
#import <SOGo/SOGoUser.h>
#import <GDLContentStore/GCSOpenIdFolder.h>
#import <GDLContentStore/GCSFolderManager.h>
@ -52,32 +55,73 @@ static BOOL SOGoOpenIDDebugEnabled = YES;
return ([sd openIdConfigUrl] && [sd openIdScope] && [sd openIdClient] && [sd openIdClientSecret]);
}
- (void) initialize
- (void) initializeWithConfig: (NSDictionary *) _config
{
SOGoSystemDefaults *sd;
id refreshTokenBool, domainInfo;
// //From sogo.conf
// openIdConfigUrl = nil;
// openIdScope = nil;
// openIdClient = nil;
// openIdClientSecret = nil;
if([_config objectForKey: @"SOGoOpenIdConfigUrl"] &&
[_config objectForKey: @"SOGoOpenIdScope"] &&
[_config objectForKey: @"SOGoOpenIdClient"] &&
[_config objectForKey: @"SOGoOpenIdClientSecret"])
{
openIdConfigUrl = [_config objectForKey: @"SOGoOpenIdConfigUrl"];
openIdScope = [_config objectForKey: @"SOGoOpenIdScope"];
openIdClient = [_config objectForKey: @"SOGoOpenIdClient"];
openIdClientSecret = [_config objectForKey: @"SOGoOpenIdClientSecret"];
openIdEmailParam = [_config objectForKey: @"SOGoOpenIdEmailParam"];
// //From request to well-known/configuration
// //SHoud be ste in sogo.cong in case of oauth
// authorizationEndpoint = nil;
// tokenEndpoint = nil;
// introspectionEndpoint = nil;
// userinfoEndpoint = nil;
// endSessionEndpoint = nil;
// revocationEndpoint = nil;
openIdEnableRefreshToken = NO;
refreshTokenBool = [_config objectForKey: @"SOGoOpenIdEnableRefreshToken"];
if (refreshTokenBool && [refreshTokenBool respondsToSelector: @selector (boolValue)])
openIdEnableRefreshToken = [refreshTokenBool boolValue];
// //Access token
// accessToken = nil;
sendDomainInfo = NO;
domainInfo = [_config objectForKey: @"SOGoOpenIdSendDomainInfo"];
if (domainInfo && [domainInfo respondsToSelector: @selector (boolValue)])
sendDomainInfo = [domainInfo boolValue];
userTokenInterval = [_config objectForKey: @"SOGoOpenIdTokenCheckInterval"];
[self _loadSessionFromCache: forDomain];
if(cacheUpdateNeeded)
{
[self fecthConfiguration: forDomain];
}
}
else
{
[self errorWithFormat: @"Missing parameters from sogo.conf"];
}
}
- (void) initialize: (NSString*) _domain
{
SOGoSystemDefaults *sd;
NSDictionary *config;
NSString *type;
sd = [SOGoSystemDefaults sharedSystemDefaults];
SOGoOpenIDDebugEnabled = [sd openIdDebugEnabled];
openIdSessionIsOK = NO;
if ([[self class] checkUserConfig])
//Check if there is a root config or config per domain
if(_domain != nil && [sd doesLoginTypeByDomain])
{
forDomain = _domain;
type = [sd getLoginTypeForDomain: _domain];
if(type != nil && [type isEqualToString: @"openid"])
{
config = [sd getLoginConfigForDomain: _domain];
[self initializeWithConfig: config];
}
else
{
[self errorWithFormat: @"Missing parameters from sogo.conf"];
}
}
else if ([[self class] checkUserConfig])
{
openIdConfigUrl = [sd openIdConfigUrl];
openIdScope = [sd openIdScope];
@ -86,12 +130,14 @@ static BOOL SOGoOpenIDDebugEnabled = YES;
openIdEmailParam = [sd openIdEmailParam];
openIdEnableRefreshToken = [sd openIdEnableRefreshToken];
userTokenInterval = [sd openIdTokenCheckInterval];
sendDomainInfo = [sd openIdSendDomainInfo];
forDomain = _domain;
[self _loadSessionFromCache];
[self _loadSessionFromCache: _domain];
if(cacheUpdateNeeded)
{
[self fecthConfiguration];
[self fecthConfiguration: _domain];
}
}
@ -101,6 +147,7 @@ static BOOL SOGoOpenIDDebugEnabled = YES;
}
}
- (WOResponse *) _performOpenIdRequest: (NSString *) endpoint
method: (NSString *) method
headers: (NSDictionary *) headers
@ -112,16 +159,16 @@ static BOOL SOGoOpenIDDebugEnabled = YES;
WOResponse *response;
WOHTTPConnection *httpConnection;
url = [NSURL URLWithString: endpoint];
if (url)
{
if(SOGoOpenIDDebugEnabled)
{
NSLog(@"OpenId perform request: %@ %@", method, [endpoint hostlessURL]);
NSLog(@"OpenId perform request: %@ %@", method, endpoint);
NSLog(@"OpenId perform request, headers %@", headers);
if(body)
NSLog(@"OpenId perform request: content %@", [[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding]);
// if(body)
// NSLog(@"OpenId perform request: content %@", [[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding]);
}
httpConnection = [[WOHTTPConnection alloc] initWithURL: url];
@ -156,13 +203,13 @@ static BOOL SOGoOpenIDDebugEnabled = YES;
}
}
- (NSMutableDictionary *) fecthConfiguration
- (NSMutableDictionary *) fecthConfiguration: (NSString*) _domain
{
NSString *location, *content;
NSString *content;
WOResponse * response;
NSUInteger status;
NSMutableDictionary *result;
NSDictionary *config;
NSDictionary *config, *headers;
NSURL *url;
result = [NSMutableDictionary dictionary];
@ -171,9 +218,14 @@ static BOOL SOGoOpenIDDebugEnabled = YES;
url = [NSURL URLWithString: self->openIdConfigUrl ];
if (url)
{
if(self->sendDomainInfo && _domain != nil && [_domain length] > 0)
headers = [NSDictionary dictionaryWithObject: _domain forKey: @"sogo-user-domain"];
else
headers = nil;
response = [self _performOpenIdRequest: self->openIdConfigUrl
method: @"GET"
headers: nil
headers: headers
body: nil];
if (response)
@ -185,12 +237,17 @@ static BOOL SOGoOpenIDDebugEnabled = YES;
config = [content objectFromJSONString];
self->authorizationEndpoint = [config objectForKey: @"authorization_endpoint"];
self->tokenEndpoint = [config objectForKey: @"token_endpoint"];
self->introspectionEndpoint = [config objectForKey: @"introspection_endpoint"];
self->userinfoEndpoint = [config objectForKey: @"userinfo_endpoint"];
self->endSessionEndpoint = [config objectForKey: @"end_session_endpoint"];
self->revocationEndpoint = [config objectForKey: @"revocation_endpoint"];
//Optionnals?
if([config objectForKey: @"introspection_endpoint"])
self->introspectionEndpoint = [config objectForKey: @"introspection_endpoint"];
if([config objectForKey: @"revocation_endpoint"])
self->revocationEndpoint = [config objectForKey: @"revocation_endpoint"];
openIdSessionIsOK = YES;
[self _saveSessionToCache];
[self _saveSessionToCache: _domain];
}
else
{
@ -206,18 +263,29 @@ static BOOL SOGoOpenIDDebugEnabled = YES;
return result;
}
+ (SOGoOpenIdSession *) OpenIdSession
+ (SOGoOpenIdSession *) OpenIdSession: (NSString *) _domain
{
SOGoOpenIdSession *newSession;
newSession = [self new];
[newSession autorelease];
[newSession initialize];
[newSession initialize: _domain];
return newSession;
}
+ (SOGoOpenIdSession *) OpenIdSessionWithToken: (NSString *) token
+ (SOGoOpenIdSession *) OpenIdSessionWithConfig: (NSDictionary *) _config
{
SOGoOpenIdSession *newSession;
newSession = [self new];
[newSession autorelease];
[newSession initializeWithConfig: _config];
return newSession;
}
+ (SOGoOpenIdSession *) OpenIdSessionWithToken: (NSString *) token domain: (NSString *) _domain
{
SOGoOpenIdSession *newSession;
@ -225,7 +293,7 @@ static BOOL SOGoOpenIDDebugEnabled = YES;
{
newSession = [self new];
[newSession autorelease];
[newSession initialize];
[newSession initialize: _domain];
[newSession setAccessToken: token];
}
@ -235,52 +303,90 @@ static BOOL SOGoOpenIDDebugEnabled = YES;
return newSession;
}
+ (SOGoOpenIdSession *) OpenIdSessionWithTokenAndConfig: (NSString *) token config: (NSDictionary *) _config
{
SOGoOpenIdSession *newSession;
if (token)
{
newSession = [self new];
[newSession autorelease];
[newSession initializeWithConfig: _config];
[newSession setAccessToken: token];
}
else
newSession = nil;
return newSession;
}
- (BOOL) sessionIsOk
{
return self->openIdSessionIsOK;
}
- (void) _loadSessionFromCache
- (void) _loadSessionFromCache: (NSString*) _domain
{
SOGoCache *cache;
NSString *jsonSession;
NSString *jsonSession, *cacheKey;
NSDictionary *sessionDict;
if(_domain != nil && [_domain length] > 0)
cacheKey = [self->openIdConfigUrl stringByAppendingFormat: @":%@", _domain];
else
cacheKey = self->openIdConfigUrl;
cache = [SOGoCache sharedCache];
jsonSession = [cache openIdSessionFromServer: self->openIdConfigUrl];
jsonSession = [cache openIdSessionFromServer: cacheKey];
if ([jsonSession length])
{
sessionDict = [jsonSession objectFromJSONString];
ASSIGN (authorizationEndpoint, [sessionDict objectForKey: @"authorization_endpoint"]);
ASSIGN (tokenEndpoint, [sessionDict objectForKey: @"token_endpoint"]);
ASSIGN (introspectionEndpoint, [sessionDict objectForKey: @"introspection_endpoint"]);
ASSIGN (userinfoEndpoint, [sessionDict objectForKey: @"userinfo_endpoint"]);
ASSIGN (endSessionEndpoint, [sessionDict objectForKey: @"end_session_endpoint"]);
ASSIGN (revocationEndpoint, [sessionDict objectForKey: @"revocation_endpoint"]);
//Optionnals?
if([sessionDict objectForKey: @"introspection_endpoint"])
ASSIGN (introspectionEndpoint, [sessionDict objectForKey: @"introspection_endpoint"]);
if([sessionDict objectForKey: @"revocation_endpoint"])
ASSIGN (revocationEndpoint, [sessionDict objectForKey: @"revocation_endpoint"]);
openIdSessionIsOK = YES;
}
else
cacheUpdateNeeded = YES;
}
- (void) _saveSessionToCache
- (void) _saveSessionToCache: (NSString*) _domain
{
SOGoCache *cache;
NSString *jsonSession;
NSString *jsonSession, *cacheKey;
NSMutableDictionary *sessionDict;
cache = [SOGoCache sharedCache];
sessionDict = [NSMutableDictionary dictionary];
[sessionDict setObject: authorizationEndpoint forKey: @"authorization_endpoint"];
[sessionDict setObject: tokenEndpoint forKey: @"token_endpoint"];
[sessionDict setObject: introspectionEndpoint forKey: @"introspection_endpoint"];
[sessionDict setObject: userinfoEndpoint forKey: @"userinfo_endpoint"];
[sessionDict setObject: endSessionEndpoint forKey: @"end_session_endpoint"];
[sessionDict setObject: revocationEndpoint forKey: @"revocation_endpoint"];
//Optionnals?
if(introspectionEndpoint)
[sessionDict setObject: introspectionEndpoint forKey: @"introspection_endpoint"];
if(revocationEndpoint)
[sessionDict setObject: revocationEndpoint forKey: @"revocation_endpoint"];
jsonSession = [sessionDict jsonRepresentation];
if(_domain != nil && [_domain length] > 0)
cacheKey = [self->openIdConfigUrl stringByAppendingFormat: @":%@", _domain];
else
cacheKey = self->openIdConfigUrl;
[cache setOpenIdSession: jsonSession
forServer: self->openIdConfigUrl];
forServer: cacheKey];
}
@ -332,6 +438,8 @@ static BOOL SOGoOpenIDDebugEnabled = YES;
logUrl = [logUrl stringByAppendingString: @"&response_type=code"];
logUrl = [logUrl stringByAppendingFormat: @"&client_id=%@", self->openIdClient];
logUrl = [logUrl stringByAppendingFormat: @"&redirect_uri=%@", oldLocation];
if(self->forDomain != nil && [self->forDomain length] > 0)
logUrl = [logUrl stringByAppendingFormat: @"&sogo_domain=%@", forDomain];
// logurl = [self->logurl stringByAppendingFormat: @"&state=%@", state];
return logUrl;
@ -397,7 +505,11 @@ static BOOL SOGoOpenIDDebugEnabled = YES;
form = [form stringByAppendingFormat: @"&client_secret=%@", self->openIdClientSecret];
form = [form stringByAppendingFormat: @"&client_id=%@", self->openIdClient];
headers = [NSDictionary dictionaryWithObject: @"application/x-www-form-urlencoded" forKey: @"content-type"];
if(self->sendDomainInfo && self->forDomain != nil && [self->forDomain length] > 0)
headers = [NSDictionary dictionaryWithObjectsAndKeys: @"application/x-www-form-urlencoded", @"content-type",
self->forDomain, @"sogo-user-domain", nil];
else
headers = [NSDictionary dictionaryWithObject: @"application/x-www-form-urlencoded" forKey: @"content-type"];
response = [self _performOpenIdRequest: location
method: @"POST"
@ -467,7 +579,11 @@ static BOOL SOGoOpenIDDebugEnabled = YES;
form = [form stringByAppendingFormat: @"&client_secret=%@", self->openIdClientSecret];
form = [form stringByAppendingFormat: @"&client_id=%@", self->openIdClient];
headers = [NSDictionary dictionaryWithObject: @"application/x-www-form-urlencoded" forKey: @"content-type"];
if(self->sendDomainInfo && self->forDomain != nil && [self->forDomain length] > 0)
headers = [NSDictionary dictionaryWithObjectsAndKeys: @"application/x-www-form-urlencoded", @"content-type",
self->forDomain, @"sogo-user-domain", nil];
else
headers = [NSDictionary dictionaryWithObject: @"application/x-www-form-urlencoded" forKey: @"content-type"];
response = [self _performOpenIdRequest: location
method: @"POST"
@ -525,7 +641,13 @@ static BOOL SOGoOpenIDDebugEnabled = YES;
if (url)
{
auth = [NSString stringWithFormat: @"Bearer %@", self->accessToken];
headers = [NSDictionary dictionaryWithObject: auth forKey: @"authorization"];
if(self->sendDomainInfo && self->forDomain != nil && [self->forDomain length] > 0)
headers = [NSDictionary dictionaryWithObjectsAndKeys: @"application/x-www-form-urlencoded", @"content-type",
self->forDomain, @"sogo-user-domain",
auth, @"authorization", nil];
else
headers = [NSDictionary dictionaryWithObjectsAndKeys: @"application/x-www-form-urlencoded", @"content-type",
auth, @"authorization", nil];
response = [self _performOpenIdRequest: location
method: @"GET"
@ -646,7 +768,7 @@ static BOOL SOGoOpenIDDebugEnabled = YES;
return @"anonymous";
}
- (BOOL) login: (NSString *) email
- (NSString *) login: (NSString *) email
{
//Check if we need to fetch userinfo
if(self->userTokenInterval > 0 && [self _loadUserFromCache: email])

View File

@ -247,12 +247,20 @@
usingKey: theKey];
r = [decodedValue rangeOfString: @":"];
*theLogin = [decodedValue substringToIndex: r.location];
*thePassword = [decodedValue substringFromIndex: r.location+1];
if (r.location != NSNotFound)
{
*theLogin = [decodedValue substringToIndex: r.location];
*thePassword = [decodedValue substringFromIndex: r.location+1];
}
else
{
*theLogin = nil;
*thePassword = nil;
}
*theDomain = nil;
sd = [SOGoSystemDefaults sharedSystemDefaults];
if ([sd enableDomainBasedUID])
if (*theLogin &&[sd enableDomainBasedUID])
{
r = [*theLogin rangeOfString: @"@" options: NSBackwardsSearch];
if (r.location != NSNotFound)

View File

@ -28,6 +28,7 @@
#import <SOGo/NSDictionary+Utilities.h>
#import <SOGo/NSString+Utilities.h>
#import <SOGo/SOGoDomainDefaults.h>
#import <SOGo/SOGoSystemDefaults.h>
#import <SOGo/SOGoUser.h>
#import <SOGo/SOGoTextTemplateFile.h>
@ -697,8 +698,11 @@ static NSString *sieveScriptName = @"sogo";
NSDictionary *result;
NSString *login, *authname, *password;
SOGoDomainDefaults *dd;
SOGoSystemDefaults *sd;
NGSieveClient *client;
NSString *sieveServer, *sieveScheme, *sieveQuery, *imapServer;
NSString *imapAuthMech, *userDomain;
NSRange r;
NSURL *url, *cUrl;
int sievePort;
BOOL connected;
@ -773,7 +777,20 @@ static NSString *sieveScriptName = @"sogo";
url = [NSURL URLWithString: [NSString stringWithFormat: @"%@://%@:%d%@",
sieveScheme, sieveServer, sievePort, sieveQuery]];
client = [[NGSieveClient alloc] initWithURL: url];
//In case of differrent auth method for different domain, check it
sd = [SOGoSystemDefaults sharedSystemDefaults];
imapAuthMech = nil;
if([sd doesLoginTypeByDomain])
{
r = [theUsername rangeOfString: @"@"];
if (r.location != NSNotFound)
{
userDomain = [theUsername substringFromIndex: r.location+1];
imapAuthMech = [sd getImapAuthMechForDomain: userDomain];
}
}
client = [[NGSieveClient alloc] initWithURL: url andAuthMech: imapAuthMech];
if (!client) {
[self errorWithFormat: @"Sieve connection failed on %@", [url description]];

View File

@ -36,6 +36,11 @@ static const NSString *kDisableSharingCalendar = @"Calendar";
+ (SOGoSystemDefaults *) sharedSystemDefaults;
- (NSArray *) domainIds;
- (BOOL) doesLoginTypeByDomain;
- (NSString *) getLoginTypeForDomain: (NSString*) _domain;
- (NSString *) getLoginConfigForDomain: (NSDictionary*) _domain;
- (NSString *) getImapAuthMechForDomain: (NSString*) _domain;
- (NSString *) getSmtpAuthMechForDomain: (NSString*) _domain;
- (BOOL) forbidUnknownDomainsAuth;
- (NSArray *) domainsAllowed;
- (BOOL) enableDomainBasedUID;
@ -90,7 +95,7 @@ NSComparisonResult languageSort(id el1, id el2, void *context);
- (NSString *) loginSuffix;
- (NSString *) authenticationType;
- (BOOL) isSsoUsed;
- (BOOL) isSsoUsed: (NSString *) domain;
- (NSString *) davAuthenticationType;
- (NSString *) CASServiceURL;
@ -102,8 +107,9 @@ NSComparisonResult languageSort(id el1, id el2, void *context);
- (NSString *) openIdClientSecret;
- (NSString *) openIdEmailParam;
- (BOOL) openIdEnableRefreshToken;
- (BOOL) openIdLogoutEnabled;
- (BOOL) openIdLogoutEnabled: (NSString *) _domain;
- (int) openIdTokenCheckInterval;
- (BOOL) openIdSendDomainInfo;
- (NSString *) SAML2PrivateKeyLocation;
- (NSString *) SAML2CertificateLocation;

View File

@ -262,6 +262,143 @@ _injectConfigurationFromFile (NSMutableDictionary *defaultsDict,
return [domains allKeys];
}
- (BOOL) doesLoginTypeByDomain
{
return ([self dictionaryForKey: @"SOGoLoginTypeByDomain"] != nil);
}
- (NSString *) getLoginTypeForDomain: (NSString*) _domain
{
NSDictionary *domains, *config;
NSString *type;
if(![self doesLoginTypeByDomain])
return nil;
domains = [self dictionaryForKey: @"SOGoLoginTypeByDomain"];
if([domains objectForKey: _domain])
{
config = [domains objectForKey: _domain];
}
else if([domains objectForKey: @"login_default"])
{
config = [domains objectForKey: @"login_default"];
}
else
return nil;
if((type = [config objectForKey: @"type"]))
{
return type;
}
else
return nil;
}
- (NSString *) getImapAuthMechForDomain: (NSString*) _domain
{
NSDictionary *domains, *config;
NSString *type;
if(![self doesLoginTypeByDomain])
return nil;
domains = [self dictionaryForKey: @"SOGoLoginTypeByDomain"];
if([domains objectForKey: _domain])
{
config = [domains objectForKey: _domain];
}
else if([domains objectForKey: @"login_default"])
{
config = [domains objectForKey: @"login_default"];
}
else
return nil;
if((type = [config objectForKey: @"imapAuthMech"]))
{
return type;
}
else
return nil;
}
- (NSString *) getSmtpAuthMechForDomain: (NSString*) _domain
{
NSDictionary *domains, *config;
NSString *type;
if(![self doesLoginTypeByDomain])
return nil;
domains = [self dictionaryForKey: @"SOGoLoginTypeByDomain"];
if([domains objectForKey: _domain])
{
config = [domains objectForKey: _domain];
}
else if([domains objectForKey: @"login_default"])
{
config = [domains objectForKey: @"login_default"];
}
else
return nil;
if((type = [config objectForKey: @"smtpAuthMech"]))
{
return type;
}
else
return nil;
}
- (NSString *) getLoginConfigForDomain: (NSDictionary*) _domain
{
NSDictionary *domains, *config;
if(![self doesLoginTypeByDomain])
return nil;
domains = [self dictionaryForKey: @"SOGoLoginTypeByDomain"];
if([domains objectForKey: _domain])
{
config = [domains objectForKey: _domain];
}
else if([domains objectForKey: @"login_default"])
{
config = [domains objectForKey: @"login_default"];
}
if(config)
return config;
else
return nil;
}
- (BOOL) hasOpenIdType
{
if([self doesLoginTypeByDomain])
{
NSDictionary *domainsConfig;
NSEnumerator *e;
NSString *domain, *type;
if(![self doesLoginTypeByDomain])
return NO;
domainsConfig = [self dictionaryForKey: @"SOGoLoginTypeByDomain"];
e = [domainsConfig keyEnumerator];
while((domain = [e nextObject]))
{
if((type = [[domainsConfig objectForKey: domain] objectForKey: @"type"]))
{
if([type isEqualToString: @"openid"])
return YES;
}
}
return NO;
}
else
return [[self authenticationType] isEqualToString: @"openid"];
}
- (BOOL) enableDomainBasedUID
{
return [self boolForKey: @"SOGoEnableDomainBasedUID"];
@ -587,11 +724,13 @@ NSComparisonResult languageSort(id el1, id el2, void *context)
return [[self stringForKey: @"SOGoAuthenticationType"] lowercaseString];
}
- (BOOL) isSsoUsed
- (BOOL) isSsoUsed: (NSString *) domain
{
NSString* authType;
authType = [self authenticationType];
authType = [self getLoginTypeForDomain: domain];
if(!authType)
authType = [self authenticationType];
return ([authType isEqualToString: @"cas"] || [authType isEqualToString: @"saml2"] || [authType isEqualToString: @"openid"]);
}
@ -640,11 +779,28 @@ NSComparisonResult languageSort(id el1, id el2, void *context)
return emailParam;
}
- (BOOL) openIdLogoutEnabled
- (BOOL) openIdLogoutEnabled: (NSString *) _domain
{
if(_domain && [self doesLoginTypeByDomain])
{
NSDictionary *config;
NSString *type;
id value;
if((config = [self getLoginConfigForDomain: _domain]))
{
if((type = [config objectForKey: @"type"]) && [type isEqualToString:@"openid"])
return [self boolForKey: @"SOGoOpenIdLogoutEnabled" andDict: config];
}
return NO;
}
return [self boolForKey: @"SOGoOpenIdLogoutEnabled"];
}
- (BOOL) openIdSendDomainInfo
{
return [self boolForKey: @"SOGoOpenIdSendDomainInfo"];
}
- (int) openIdTokenCheckInterval
{

View File

@ -91,6 +91,7 @@
/* properties */
- (NSString *) domain;
- (NSString *) loginDomain;
- (id <SOGoSource>) authenticationSource;
- (NSArray *) allEmails;

View File

@ -301,6 +301,19 @@ static const NSString *kEncryptedUserNamePrefix = @"uenc";
return [self _fetchFieldForUser: @"c_domain"];
}
- (NSString *) loginDomain
{
NSRange r;
NSString *domain = nil;
r = [self->login rangeOfString: @"@"];
if (r.location != NSNotFound)
{
domain = [self->login substringFromIndex: r.location+1];
}
return domain;
}
- (id <SOGoSource>) authenticationSource
{
NSString *sourceID;

View File

@ -27,6 +27,7 @@
#import "SOGoConstants.h"
@class NSString;
@class NSMutableDictionary;
@class WOContext;
@class WOCookie;

View File

@ -138,11 +138,33 @@
SOGoOpenIdSession * openIdSession;
SOGoSystemDefaults *sd;
NSString *authenticationType;
NSString* loginDomain;
BOOL rc;
sd = [SOGoSystemDefaults sharedSystemDefaults];
//Basic check
if(!_login)
return NO;
if(_login && [_login length] == 0)
return NO;
loginDomain = nil;
if(*_domain == nil || [*_domain length] == 0)
{
NSRange r;
r = [_login rangeOfString: @"@"];
if (r.location != NSNotFound)
{
loginDomain = [_login substringFromIndex: r.location+1];
}
}
if([sd doesLoginTypeByDomain])
authenticationType = [sd getLoginTypeForDomain: loginDomain];
else
authenticationType = [sd authenticationType];
authenticationType = [sd authenticationType];
if ([authenticationType isEqualToString: @"cas"])
{
casSession = [SOGoCASSession CASSessionWithIdentifier: _pwd fromProxy: NO];
@ -153,7 +175,7 @@
}
else if ([authenticationType isEqualToString: @"openid"])
{
openIdSession = [SOGoOpenIdSession OpenIdSessionWithToken: _pwd];
openIdSession = [SOGoOpenIdSession OpenIdSessionWithToken: _pwd domain: loginDomain];
if (openIdSession)
rc = [[openIdSession login: _login] isEqualToString: _login];
else
@ -180,7 +202,6 @@
grace: _grace
additionalInfo: _additionalInfo
useCache: _useCache];
//[self logWithFormat: @"Checked login with ppolicy enabled: %d %d %d", *_perr, *_expire, *_grace];
// It's important to return the real value here. The callee will handle
@ -259,7 +280,8 @@
login: &login
domain: &domain
password: &pwd];
if (![self checkLogin: login
password: pwd
domain: &domain
@ -282,32 +304,42 @@
{
NSString *authType, *password;
SOGoSystemDefaults *sd;
SOGoUser *user;
NSRange r;
NSString *loginDomain, *login;
password = [self passwordInContext: context];
if ([password length])
{
user = [self userInContext: context];
login = [user loginInDomain];
r = [login rangeOfString: @"@"];
if (r.location != NSNotFound)
loginDomain = [login substringFromIndex: r.location+1];
else
loginDomain = nil;
sd = [SOGoSystemDefaults sharedSystemDefaults];
authType = [sd authenticationType];
if([sd doesLoginTypeByDomain])
authType = [sd getLoginTypeForDomain: loginDomain];
else
authType = [sd authenticationType];
if ([authType isEqualToString: @"cas"])
{
SOGoCASSession *session;
SOGoUser *user;
NSString *service, *scheme;
session = [SOGoCASSession CASSessionWithIdentifier: password
fromProxy: NO];
user = [self userInContext: context];
// Try configured CAS service name first
service = [[user domainDefaults] imapCASServiceName];
if (!service)
{
// We must NOT assume the scheme exists
scheme = [server scheme];
if (!scheme)
scheme = @"imap";
service = [NSString stringWithFormat: @"%@://%@",
scheme, [server host]];
}
@ -316,17 +348,16 @@
[session invalidateTicketForService: service];
password = [session ticketForService: service];
if ([password length] || renew)
[session updateCache];
}
else if ([authType isEqualToString: @"openid"])
{
SOGoOpenIdSession* session;
NSString* currentToken;
//If the token has been refresh during the request, we need to use the new access_token
//as the one from the cookie is no more valid
session = [SOGoOpenIdSession OpenIdSessionWithToken: password];
session = [SOGoOpenIdSession OpenIdSessionWithToken: password domain: loginDomain];
password = [session getCurrentToken];
}
#if defined(SAML2_CONFIG)
@ -351,6 +382,16 @@
return password;
}
- (NSString *) smtpPasswordInContext: (WOContext *) context
forURL: (NSURL *) server
{
NSString *password;
password = [self imapPasswordInContext: context forURL: server forceRenew:NO];
return password;
}
/* create SOGoUser */
- (SOGoUser *) userWithLogin: (NSString *) login
@ -459,21 +500,36 @@
{
NSArray *listCookies = nil;
SOGoSystemDefaults *sd;
NSString *authType;
NSString *authType, *username, *login, *loginDomain;
NSRange r;
SOGoUser *user;
user = [self userInContext: _ctx];
login = [user loginDomain];
r = [login rangeOfString: @"@"];
if (r.location != NSNotFound)
loginDomain = [login substringFromIndex: r.location+1];
else
loginDomain = nil;
sd = [SOGoSystemDefaults sharedSystemDefaults];
authType = [sd authenticationType];
if(loginDomain && [sd doesLoginTypeByDomain])
authType = [sd getLoginTypeForDomain: loginDomain];
else
authType = [sd authenticationType];
if([authType isEqualToString:@"openid"] && [sd openIdEnableRefreshToken])
{
NSString *currentPassword, *newPassword, *username;
NSString *currentPassword, *newPassword;
SOGoOpenIdSession *openIdSession;
WOCookie* newCookie;
currentPassword = [self passwordInContext: _ctx];
newPassword = [self imapPasswordInContext: _ctx forURL: nil forceRenew: NO];
if(currentPassword && newPassword && ![newPassword isEqualToString: currentPassword])
{
openIdSession = [SOGoOpenIdSession OpenIdSessionWithToken: newPassword];
openIdSession = [SOGoOpenIdSession OpenIdSessionWithToken: newPassword domain: loginDomain];
if (openIdSession)
username = [openIdSession login: @""]; //Force to refresh the name
else

View File

@ -21,6 +21,7 @@ $(SOGO_TOOL)_OBJC_FILES += \
SOGoToolRemoveDoubles.m \
SOGoToolRenameUser.m \
SOGoToolCheckupUser.m \
SOGoToolCleanOpenIdSessions.m \
SOGoToolCleanupUser.m \
SOGoToolRestore.m \
SOGoToolCreateFolder.m \

View File

@ -0,0 +1,137 @@
/* SOGoToolCleanOpenIdSessions.m - this file is part of SOGo
*
* Copyright (C) 2012-2021 Inverse inc.
*
* This file is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This file is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
* Boston, MA 02111-1307, USA.
*/
#import <Foundation/NSArray.h>
#import <Foundation/NSCalendarDate.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSException.h>
#import <Foundation/NSUserDefaults.h>
#import <GDLAccess/EOAdaptorChannel.h>
#import <GDLContentStore/GCSChannelManager.h>
#import <GDLContentStore/NSURL+GCS.h>
#import <NGExtensions/NSNull+misc.h>
#import <SOGo/NSString+Utilities.h>
#import <SOGo/SOGoSession.h>
#import <SOGo/SOGoOpenIdSession.h>
#import "SOGoTool.h"
@interface SOGoToolCleanOpenIdSessions : SOGoTool
@end
@implementation SOGoToolCleanOpenIdSessions
+ (NSString *) command
{
return @"clean-openid-sessions";
}
+ (NSString *) description
{
return @"clean user openid sessions that are expired";
}
- (void) usage
{
fprintf (stderr, "clean-openid-sessions\n\n"
"\n"
"The clean-openid-sessions action should be configured as a cronjob.\n");
}
- (BOOL) cleanExpiredOpenIdSession
{
BOOL rc;
EOAdaptorChannel *channel;
GCSChannelManager *cm;
NSArray *attrs;
NSDictionary *qresult;
NSException *ex;
NSString *sql, *sessionsFolderURL, *sessionID;
NSURL *tableURL;
NSUserDefaults *ud;
unsigned int now;
rc = YES;
ud = [NSUserDefaults standardUserDefaults];
now = [[NSCalendarDate calendarDate] timeIntervalSince1970];
sessionID = nil;
sessionsFolderURL = [ud stringForKey: @"OCSOpenIdURL"];
if (!sessionsFolderURL)
{
if (verbose)
NSLog(@"Couldn't read OCSOpenIdURL");
return rc = NO;
}
tableURL = [[NSURL alloc] initWithString: sessionsFolderURL];
cm = [GCSChannelManager defaultChannelManager];
channel = [cm acquireOpenChannelForURL: tableURL];
if (!channel)
{
/* FIXME: nice error msg */
NSLog(@"Can't acquire channel");
return rc = NO;
}
sql = [NSString stringWithFormat: @"SELECT c_user_session FROM %@ WHERE c_access_token_expires_in <= %d AND c_refresh_token_expires_in <= %d",
[tableURL gcsTableName], now, now];
ex = [channel evaluateExpressionX: sql];
if (ex)
{
NSLog(@"%@", [ex reason]);
[ex raise];
return rc = NO;
}
attrs = [channel describeResults: NO];
while ((qresult = [channel fetchAttributes: attrs withZone: NULL]))
{
sessionID = [qresult objectForKey: @"c_user_session"];
if (sessionID)
{
if (verbose)
NSLog(@"Removing session %@", sessionID);
[SOGoOpenIdSession deleteValueForSessionKey: sessionID];
}
}
[cm releaseChannel: channel immediately: YES];
if (verbose && sessionID == nil)
NSLog(@"No session to remove on openId table");
return rc;
}
- (BOOL) run
{
BOOL rc;
rc = [self cleanExpiredOpenIdSession];
return rc;
}
@end

View File

@ -60,73 +60,6 @@
"The expire-sessions action should be configured as a cronjob.\n");
}
- (BOOL) expireUserOpenIdSessionOlderThan: (int) nbMinutes
{
BOOL rc;
EOAdaptorChannel *channel;
GCSChannelManager *cm;
NSArray *attrs;
NSDictionary *qresult;
NSException *ex;
NSString *sql, *sessionsFolderURL, *sessionID;
NSURL *tableURL;
NSUserDefaults *ud;
unsigned int now, oldest;
rc = YES;
ud = [NSUserDefaults standardUserDefaults];
now = [[NSCalendarDate calendarDate] timeIntervalSince1970];
oldest = now - (nbMinutes * 60);
sessionID = nil;
sessionsFolderURL = [ud stringForKey: @"OCSOpenIdURL"];
if (!sessionsFolderURL)
{
if (verbose)
NSLog(@"Couldn't read OCSOpenIdURL");
return rc = NO;
}
tableURL = [[NSURL alloc] initWithString: sessionsFolderURL];
cm = [GCSChannelManager defaultChannelManager];
channel = [cm acquireOpenChannelForURL: tableURL];
if (!channel)
{
/* FIXME: nice error msg */
NSLog(@"Can't aquire channel");
return rc = NO;
}
sql = [NSString stringWithFormat: @"SELECT c_user_session FROM %@ WHERE c_access_token_expires_in <= %d",
[tableURL gcsTableName], oldest];
ex = [channel evaluateExpressionX: sql];
if (ex)
{
NSLog(@"%@", [ex reason]);
[ex raise];
return rc = NO;
}
attrs = [channel describeResults: NO];
while ((qresult = [channel fetchAttributes: attrs withZone: NULL]))
{
sessionID = [qresult objectForKey: @"c_user_session"];
if (sessionID)
{
if (verbose)
NSLog(@"Removing session %@", sessionID);
[SOGoOpenIdSession deleteValueForSessionKey: sessionID];
}
}
[cm releaseChannel: channel immediately: YES];
if (verbose && sessionID == nil)
NSLog(@"No session to remove on openId");
return rc;
}
- (BOOL) expireUserSessionOlderThan: (int) nbMinutes
{
BOOL rc;
@ -135,7 +68,7 @@
NSArray *attrs;
NSDictionary *qresult;
NSException *ex;
NSString *sql, *sessionsFolderURL, *sessionID, *authType;
NSString *sql, *sessionsFolderURL, *sessionID;
NSURL *tableURL;
NSUserDefaults *ud;
@ -191,13 +124,6 @@
if (verbose && sessionID == nil)
NSLog(@"No session to remove");
//doing openid session if needed
authType = [ud stringForKey:@"SOGoAuthenticationType"];
if([authType isEqualToString: @"openid"])
{
[self expireUserOpenIdSessionOlderThan: nbMinutes];
}
return rc;
}

View File

@ -488,14 +488,6 @@
&& [user isSuperUser]);
}
- (BOOL) usesCASAuthentication
{
SOGoSystemDefaults *sd;
sd = [SOGoSystemDefaults sharedSystemDefaults];
return [[sd authenticationType] isEqualToString: @"cas"];
}
- (BOOL) usesOpenIdAuthentication
{
@ -546,19 +538,32 @@
BOOL canLogoff;
id auth;
SOGoSystemDefaults *sd;
NSString *authType;
NSString *authType, *login, *loginDomain;
NSRange r;
auth = [[self clientObject] authenticatorInContext: context];
if ([auth respondsToSelector: @selector (cookieNameInContext:)])
{
sd = [SOGoSystemDefaults sharedSystemDefaults];
authType = [sd authenticationType];
login = [[context activeUser] login];
r = [login rangeOfString: @"@"];
if (r.location != NSNotFound)
loginDomain = [login substringFromIndex: r.location+1];
else
loginDomain = nil;
if(loginDomain && [sd doesLoginTypeByDomain])
authType = [sd getLoginTypeForDomain: loginDomain];
else
authType = [sd authenticationType];
if ([authType isEqualToString: @"cas"])
canLogoff = [sd CASLogoutEnabled];
else if ([authType isEqualToString: @"saml2"])
canLogoff = [sd SAML2LogoutEnabled];
else if ([authType isEqualToString: @"openid"])
canLogoff = [sd openIdLogoutEnabled];
canLogoff = [sd openIdLogoutEnabled: loginDomain];
else
canLogoff = [[auth cookieNameInContext: context] length] > 0;
}

View File

@ -146,6 +146,7 @@ static const NSString *kJwtKey = @"jwt";
- (WOCookie *) _authLocationCookie: (BOOL) cookieReset
withName: (NSString *) cookieName
withValue: (NSString *) _value
{
WOCookie *locationCookie;
NSString *appName;
@ -153,7 +154,10 @@ static const NSString *kJwtKey = @"jwt";
NSCalendarDate *date;
rq = [context request];
locationCookie = [WOCookie cookieWithName: cookieName value: [rq uri]];
if(_value)
locationCookie = [WOCookie cookieWithName: cookieName value: _value];
else
locationCookie = [WOCookie cookieWithName: cookieName value: [rq uri]];
appName = [rq applicationName];
[locationCookie setPath: [NSString stringWithFormat: @"/%@/", appName]];
if (cookieReset)
@ -166,6 +170,28 @@ static const NSString *kJwtKey = @"jwt";
return locationCookie;
}
- (WOCookie *) _domainCookie: (BOOL) cookieReset
withDomain: (NSString *) _domain
{
WOCookie *domainCookie;
NSString *appName;
WORequest *rq;
NSCalendarDate *date;
rq = [context request];
domainCookie = [WOCookie cookieWithName: @"sogo-user-domain" value: _domain];
appName = [rq applicationName];
[domainCookie setPath: [NSString stringWithFormat: @"/%@/", appName]];
if (cookieReset)
{
date = [NSCalendarDate calendarDate];
[date setTimeZone: [NSTimeZone timeZoneForSecondsFromGMT: 0]];
[domainCookie setExpires: [date yesterday]];
}
return domainCookie;
}
//
//
//
@ -426,6 +452,7 @@ static const NSString *kJwtKey = @"jwt";
return response;
}
- (NSDictionary *) _casRedirectKeys
{
NSDictionary *redirectKeys;
@ -499,7 +526,8 @@ static const NSString *kJwtKey = @"jwt";
/* login callback, we expire the "cas-location" cookie, created
below */
casLocationCookie = [self _authLocationCookie: YES
withName: @"cas-location"];
withName: @"cas-location"
withValue: nil];
}
}
else
@ -538,7 +566,8 @@ static const NSString *kJwtKey = @"jwt";
newLocation = [SOGoCASSession CASURLWithAction: @"login"
andParameters: [self _casRedirectKeys]];
casLocationCookie = [self _authLocationCookie: NO
withName: @"cas-location"];
withName: @"cas-location"
withValue: nil];
}
response = [self redirectToLocation: newLocation];
if (casCookie)
@ -549,7 +578,7 @@ static const NSString *kJwtKey = @"jwt";
return response;
}
- (id <WOActionResults>) _openidDefaultAction
- (id <WOActionResults>) _openidDefaultAction: (NSString *) _domain
{
WOResponse *response;
NSString *login, *redirectLocation, *serverUrl;
@ -557,7 +586,7 @@ static const NSString *kJwtKey = @"jwt";
NSURL *newLocation, *oldLocation;
NSDictionary *formValues;
SOGoUser *loggedInUser;
WOCookie *openIdCookie, *openIdCookieLocation, *openIdRefreshCookie;
WOCookie *openIdCookie, *openIdCookieLocation, *openIdRefreshCookie, *domainCookie;
WORequest *rq;
SOGoWebAuthenticator *auth;
SOGoOpenIdSession *openIdSession;
@ -566,9 +595,16 @@ static const NSString *kJwtKey = @"jwt";
openIdCookie = nil;
openIdCookieLocation = nil;
openIdRefreshCookie = nil;
domainCookie = nil;
newLocation = nil;
openIdSession = [SOGoOpenIdSession OpenIdSession];
rq = [context request];
//Check if the domain is stored in a cookie if not given
if(_domain == nil || [_domain length] == 0)
_domain = [rq cookieValueForKey: @"sogo-user-domain"]; //_domain can still be nil aftert his
openIdSession = [SOGoOpenIdSession OpenIdSession: _domain];
if(![openIdSession sessionIsOk])
{
@ -577,7 +613,6 @@ static const NSString *kJwtKey = @"jwt";
}
login = [[context activeUser] login];
rq = [context request];
if ([login isEqualToString: @"anonymous"])
login = nil;
if (!login)
@ -585,7 +620,6 @@ static const NSString *kJwtKey = @"jwt";
//You get here if you nerver been logged in or if you token is expired
serverUrl = [[context serverURL] absoluteString];
redirectLocation = [NSString stringWithFormat: @"%@/%@/", serverUrl, [rq applicationName]];
NSLog(@"ServerUrl %@ and redirect: %@", serverUrl, redirectLocation);
if((formValues = [rq formValues]) && [formValues objectForKey: @"code"])
{
//You get here if this is the callback of openid after you logged in
@ -596,6 +630,7 @@ static const NSString *kJwtKey = @"jwt";
// sessionState = [value lastObject];
// else
// sessionState = value;
value = [formValues objectForKey: @"code"];
if ([value isKindOfClass: [NSArray class]])
code = [value lastObject];
@ -611,7 +646,8 @@ static const NSString *kJwtKey = @"jwt";
inContext: context];
}
newLocation = [rq cookieValueForKey: @"openid-location"];
openIdCookieLocation = [self _authLocationCookie: YES withName: @"openid-location"];
openIdCookieLocation = [self _authLocationCookie: YES withName: @"openid-location" withValue: nil];
domainCookie = [self _domainCookie: YES withDomain: _domain];
}
// else if((formValues = [rq formValues]) && [formValues objectForKey: @"action"])
// {
@ -633,8 +669,13 @@ static const NSString *kJwtKey = @"jwt";
// //To avoid making a redirection to openid server after a post request, we first redirect to a get method
// newLocation = [NSString stringWithFormat: @"%@?action=redirect", redirectLocation];
// else
newLocation = [openIdSession loginUrl: redirectLocation];
openIdCookieLocation = [self _authLocationCookie: NO withName: @"openid-location"];
if(_domain != nil && [_domain length] > 0)
{
//add the domain cookie to get it after the redirect
domainCookie = [self _domainCookie: NO withDomain: _domain];
}
newLocation = [openIdSession loginUrl: redirectLocation];
openIdCookieLocation = [self _authLocationCookie: NO withName: @"openid-location" withValue: nil];
}
}
else
@ -654,10 +695,14 @@ static const NSString *kJwtKey = @"jwt";
[response addCookie: openIdCookie];
if (openIdCookieLocation)
[response addCookie: openIdCookieLocation];
if(domainCookie)
[response addCookie: domainCookie];
//[response setStatus: 303];
return response;
}
#if defined(SAML2_CONFIG)
- (id <WOActionResults>) _saml2DefaultAction
{
@ -681,7 +726,8 @@ static const NSString *kJwtKey = @"jwt";
newLocation = [rq cookieValueForKey: @"saml2-location"];
if (newLocation)
saml2LocationCookie = [self _authLocationCookie: YES
withName: @"saml2-location"];
withName: @"saml2-location"
withValue: nil];
else
{
oldLocation = [[self clientObject] baseURLInContext: context];
@ -696,7 +742,8 @@ static const NSString *kJwtKey = @"jwt";
{
newLocation = [SOGoSAML2Session authenticationURLInContext: context];
saml2LocationCookie = [self _authLocationCookie: NO
withName: @"saml2-location"];
withName: @"saml2-location"
withValue: nil];
}
response = [self redirectToLocation: newLocation];
@ -717,13 +764,11 @@ static const NSString *kJwtKey = @"jwt";
login = nil;
if (login)
{
oldLocation = [[self clientObject] baseURLInContext: context];
response
= [self redirectToLocation: [NSString stringWithFormat: @"%@%@",
oldLocation,
[[SOGoUser getEncryptedUsernameIfNeeded:login request: [context request]] stringByEscapingURL]]];
}
{
oldLocation = [[self clientObject] baseURLInContext: context];
response = [self redirectToLocation: [NSString stringWithFormat: @"%@%@", oldLocation,
[[SOGoUser getEncryptedUsernameIfNeeded:login request: [context request]] stringByEscapingURL]]];
}
else
{
oldLocation = [[context request] uri];
@ -736,23 +781,174 @@ static const NSString *kJwtKey = @"jwt";
return response;
}
- (WOResponse *) connectNameAction
{
WOResponse *response;
WORequest *request;
NSDictionary *params;
NSString *username, *language, *domain, *type, *serverUrl, *redirectLocation;
NSRange r;
request = [context request];
params = [[request contentAsString] objectFromJSONString];
username = [params objectForKey: @"userName"];
//Extract the domain
r = [username rangeOfString: @"@"];
if (r.location != NSNotFound)
{
domain = [username substringFromIndex: r.location+1];
type = [[SOGoSystemDefaults sharedSystemDefaults] getLoginTypeForDomain: domain];
if(type != nil)
{
if([type isEqualToString: @"plain"])
{
//Only reload the page with the name
serverUrl = [[context serverURL] absoluteString];
redirectLocation = [NSString stringWithFormat: @"%@/%@/login?hint=%@", serverUrl, [request applicationName], username];
//response = [self redirectToLocation: [NSString stringWithFormat: @"%@/", redirectLocation]];
response = [self responseWithStatus: 200 andJSONRepresentation:
[NSDictionary dictionaryWithObjectsAndKeys: redirectLocation, @"redirect", nil]];
}
else if([type isEqualToString: @"openid"])
{
SOGoOpenIdSession *openIdSession;
WOCookie *domainCookie, *openIdCookieLocation;
//With openId, the user will be redirected to the openid server for login
//With set the domain in a cookie to know it after the openid does the callbacl
serverUrl = [[context serverURL] absoluteString];
redirectLocation = [NSString stringWithFormat: @"%@/%@/", serverUrl, [request applicationName]];
openIdSession = [SOGoOpenIdSession OpenIdSession: domain];
domainCookie = [self _domainCookie: NO withDomain: domain];
openIdCookieLocation = [self _authLocationCookie: NO withName: @"openid-location" withValue: redirectLocation];
response = [self responseWithStatus: 200 andJSONRepresentation:
[NSDictionary dictionaryWithObjectsAndKeys: [openIdSession loginUrl: redirectLocation], @"redirect", nil]];
[response addCookie: domainCookie];
[response addCookie: openIdCookieLocation];
}
else if([type isEqualToString: @"cas"] || [type isEqualToString: @"saml2"])
{
[self logWithFormat: @"Unsupported type for now: %@", type];
response = [self responseWithStatus: 400
andString: @"Domain Authentication type not supported"];
}
else
{
[self logWithFormat: @"Unknown type: %@", type];
response = [self responseWithStatus: 400
andString: @"Unknwon Authentication type"];
}
}
else
{
[self logWithFormat: @"Auth type for Domain given is not set or there is no default value: %@", domain];
response = [self responseWithStatus: 400
andString: @"Domain unknown"];
}
}
else
{
[self logWithFormat: @"Domain is required but not found for user recovery exception for user %@", username];
response = [self responseWithStatus: 400
andString: @"Domain needed in the login"];
}
return response;
}
- (id <WOActionResults>) defaultAction
{
NSString *authenticationType;
NSString *authenticationType, *loginDomain, *type, *_domain;
SOGoSystemDefaults* sd;
id <WOActionResults> result;
authenticationType = [[SOGoSystemDefaults sharedSystemDefaults]
authenticationType];
if ([authenticationType isEqualToString: @"cas"])
result = [self _casDefaultAction];
else if ([authenticationType isEqualToString: @"openid"])
result = [self _openidDefaultAction];
#if defined(SAML2_CONFIG)
else if ([authenticationType isEqualToString: @"saml2"])
result = [self _saml2DefaultAction];
#endif /* SAML2_CONFIG */
else
result = [self _standardDefaultAction];
loginDomain = nil;
sd = [SOGoSystemDefaults sharedSystemDefaults];
if([sd doesLoginTypeByDomain])
{
NSString *login;
//In this mode sogo will ask the mail of the user before doing any authentication
//Check if a user is already logged in
_domain = [[context request] cookieValueForKey: @"sogo-user-domain"]; //_domain can still be nil aftert his
if(_domain != nil)
{
//This is a callback of an openid session.
return [self _openidDefaultAction: _domain];
}
login = [[context activeUser] login];
if ([login isEqualToString: @"anonymous"])
login = nil;
if(!login && !_domain)
return [self _standardDefaultAction];
else
{
//User already logged in. Extract the domain in that case
NSRange r;
r = [login rangeOfString: @"@"];
if (r.location != NSNotFound)
{
loginDomain = [login substringFromIndex: r.location+1];
type = [sd getLoginTypeForDomain: loginDomain];
if(type)
{
if([type isEqualToString: @"plain"])
{
result = [self _standardDefaultAction];
}
else if([type isEqualToString: @"openid"])
{
result = [self _openidDefaultAction: loginDomain];
}
else if([type isEqualToString: @"cas"] || [type isEqualToString: @"saml2"])
{
[self logWithFormat: @"Unsupported type for now: %@", type];
result = [self responseWithStatus: 400
andString: @"Domain Authentication type not supported"];
}
else
{
[self logWithFormat: @"Unknown type: %@", type];
result = [self responseWithStatus: 400
andString: @"Unknwon Authentication type"];
}
}
else
{
[self logWithFormat: @"Auth type for Domain given is not set or there is no default value: %@", loginDomain];
result = [self responseWithStatus: 400
andString: @"Domain unknown"];
}
}
else
{
loginDomain = nil;
result = [self _standardDefaultAction];
}
}
}
else {
authenticationType = [sd authenticationType];
if ([authenticationType isEqualToString: @"cas"])
result = [self _casDefaultAction];
else if ([authenticationType isEqualToString: @"openid"])
result = [self _openidDefaultAction: loginDomain];
#if defined(SAML2_CONFIG)
else if ([authenticationType isEqualToString: @"saml2"])
result = [self _saml2DefaultAction];
#endif /* SAML2_CONFIG */
else
result = [self _standardDefaultAction];
}
return result;
}
@ -782,6 +978,57 @@ static const NSString *kJwtKey = @"jwt";
return ([[self loginDomains] count] > 0);
}
- (BOOL) doLoginUsernameFirst
{
return [[SOGoSystemDefaults sharedSystemDefaults] doesLoginTypeByDomain];
}
- (BOOL) doFullLogin
{
//Either we directly do the full login (meaning the user inputs its username and password)
//Or we do it in two times:
//phase 1: user types its username first -> only show the username input
//phase 2: user types its password -> show all inputs
//In phase 2, the username will be in the query at key "login"
if([self doLoginUsernameFirst]){
WORequest *rq;
BOOL hasLogin;
NSDictionary *formValues;
rq = [context request];
hasLogin = ((formValues=[rq formValues]) && [formValues objectForKey: @"hint"]);
return hasLogin;
}
return YES;
}
- (BOOL) doPartialLogin
{
return ![self doFullLogin];
}
- (NSString *) getLoginHint
{
id value;
WORequest *rq;
NSString* login;
NSDictionary *formValues;
login = @"";
rq = [context request];
if((formValues=[rq formValues]) && (value=[formValues objectForKey: @"hint"]))
{
if ([value isKindOfClass: [NSArray class]])
login = [value lastObject];
else
login = value;
}
return login;
}
- (BOOL) hasPasswordRecovery
{
return [[SOGoSystemDefaults sharedSystemDefaults] isPasswordRecoveryEnabled];
@ -1145,8 +1392,7 @@ static const NSString *kJwtKey = @"jwt";
message = [[request contentAsString] objectFromJSONString];
username = [message objectForKey: @"userName"];
domain = [message objectForKey: @"domain"];
if ([[SOGoSystemDefaults sharedSystemDefaults]
isPasswordRecoveryEnabled]) {
if ([[SOGoSystemDefaults sharedSystemDefaults] isPasswordRecoveryEnabled]) {
// If no domain, try to retrieve domain from username
if (nil != domain && domain != [NSNull null]) {
domainName = domain;

View File

@ -420,26 +420,38 @@
- (NSString *) _logoutRedirectURL
{
NSString *redirectURL;
NSString *redirectURL, *login, *loginDomain, *authType;
SOGoSystemDefaults *sd;
id container;
NSRange r;
login = [[context activeUser] login];
r = [login rangeOfString: @"@"];
if (r.location != NSNotFound)
loginDomain = [login substringFromIndex: r.location+1];
else
loginDomain = nil;
sd = [SOGoSystemDefaults sharedSystemDefaults];
if ([[sd authenticationType] isEqualToString: @"cas"])
{
redirectURL = [SOGoCASSession CASURLWithAction: @"logout"
andParameters: nil];
}
else if ([[sd authenticationType] isEqualToString: @"openid"])
if(loginDomain && [sd doesLoginTypeByDomain])
authType = [sd getLoginTypeForDomain: loginDomain];
else
authType = [sd authenticationType];
if ([authType isEqualToString: @"cas"])
{
redirectURL = [SOGoCASSession CASURLWithAction: @"logout"
andParameters: nil];
}
else if ([authType isEqualToString: @"openid"])
{
SOGoOpenIdSession* session;
session = [SOGoOpenIdSession OpenIdSession];
session = [SOGoOpenIdSession OpenIdSession: loginDomain];
redirectURL = [session logoutUrl];
//delete openid session in database
}
#if defined(SAML2_CONFIG)
else if ([[sd authenticationType] isEqualToString: @"saml2"])
else if ([authType isEqualToString: @"saml2"])
{
NSString *username, *password, *domain, *value;
SOGoSAML2Session *saml2Session;

View File

@ -168,6 +168,16 @@
pageName = "SOGoRootPage";
actionName = "connect";
};
connectName = {
protectedBy = "<public>";
pageName = "SOGoRootPage";
actionName = "connectName";
};
openid_redirect = {
protectedBy = "<public>";
pageName = "SOGoRootPage";
actionName = "openIdRedirect";
};
changePassword = {
protectedBy = "<public>";
pageName = "SOGoRootPage";

View File

@ -12,6 +12,7 @@
<script type="text/javascript">
var cookieUsername = <var:string var:value="cookieUsername.doubleQuotedString" const:escapeHTML="NO"/>;
var language = '<var:string var:value="language" const:escapeHTML="NO"/>';
var loginHint = '<var:string var:value="getLoginHint" const:escapeHTML="NO"/>'
</script>
<!--
@ -44,116 +45,121 @@
</div>
</var:if>
</div>
<div class="sg-login md-default-theme md-bg md-accent" flex-gt-md="50">
<div id="login" class="sg-login-content md-padding">
<form name="loginForm" layout="column"
ng-cloak="ng-cloak"
ng-submit="app.login()">
<var:if condition="hasLoginSuffix">
<input type="hidden" ng-model="app.creds.loginSuffix" var:value="loginSuffix"/>
</var:if>
<div ng-if="!app.loginState">
<md-input-container class="md-block">
<label><var:string label:value="Username"/></label>
<md-icon>person</md-icon>
<input autocorrect="off" autocapitalize="off" type="text" ng-model="app.creds.username" ng-required="true" ng-change="app.usernameChanged()" ng-blur="app.retrievePasswordRecoveryEnabled()" />
</md-input-container>
<md-input-container class="md-block">
<label><var:string label:value="Password"/></label>
<md-icon>vpn_key</md-icon>
<input id="passwordField" type="password" ng-model="app.creds.password" ng-required="true"/>
<md-icon id="password-visibility-icon" ng-click="app.changePasswordVisibility()">visibility</md-icon>
</md-input-container>
<var:if condition="doFullLogin">
<div id="login" class="sg-login-content md-padding">
<form name="loginForm" layout="column"
ng-cloak="ng-cloak"
ng-submit="app.login()">
<var:if condition="hasLoginSuffix">
<input type="hidden" ng-model="app.creds.loginSuffix" var:value="loginSuffix"/>
</var:if>
<div ng-if="!app.loginState">
<!-- LANGUAGES SELECT -->
<div layout="row" layout-align="start end">
<md-icon>language</md-icon>
<md-input-container class="md-flex">
<label><var:string label:value="choose"/></label>
<md-select ng-model="app.creds.language"
var:placeholder="localizedLanguage"
ng-change="app.changeLanguage($event)">
<var:foreach list="languages" item="item">
<md-option var:value="item">
<var:string value="languageText"/>
</md-option>
</var:foreach>
</md-select>
<md-input-container class="md-block">
<label><var:string label:value="Username"/></label>
<md-icon>person</md-icon>
<input autocorrect="off" autocapitalize="off" type="text" ng-model="app.creds.username" ng-required="true" ng-change="app.usernameChanged()" ng-blur="app.retrievePasswordRecoveryEnabled()" />
</md-input-container>
</div>
<!-- DOMAINS SELECT -->
<var:if condition="hasLoginDomains">
<md-input-container class="md-block">
<label><var:string label:value="Password"/></label>
<md-icon>vpn_key</md-icon>
<input id="passwordField" type="password" ng-model="app.creds.password" ng-required="true"/>
<md-icon id="password-visibility-icon" ng-click="app.changePasswordVisibility()">visibility</md-icon>
</md-input-container>
<!-- LANGUAGES SELECT -->
<div layout="row" layout-align="start end">
<md-icon>domain</md-icon>
<md-input-container class="md-flex">
<md-select class="md-flex" ng-model="app.creds.domain" label:placeholder="choose" ng-change="app.retrievePasswordRecoveryEnabled()">
<var:foreach list="loginDomains" item="item">
<md-option var:value="item">
<var:string value="item"/>
</md-option>
</var:foreach>
</md-select>
</md-input-container>
</div>
</var:if>
<div layout="row" layout-align="center center">
<md-switch class="md-accent md-hue-2"
ng-model="app.creds.rememberLogin"
label:arial-label="Remember username">
<var:string label:value="Remember username"/>
</md-switch>
</div>
<var:if condition="hasUrlCreateAccount">
<div layout="row" layout-align="center center">
<a var:href="urlCreateAccount" target="_blank" class="create-account-link"><var:string label:value="Create an account"/></a>
<md-icon>language</md-icon>
<md-input-container class="md-flex">
<label><var:string label:value="choose"/></label>
<md-select ng-model="app.creds.language"
var:placeholder="localizedLanguage"
ng-change="app.changeLanguage($event)">
<var:foreach list="languages" item="item">
<md-option var:value="item">
<var:string value="languageText"/>
</md-option>
</var:foreach>
</md-select>
</md-input-container>
</div>
</var:if>
</div>
<!-- Password recovery -->
<div layout="row" layout-align="center center" ng-if="app.passwordRecovery.passwordRecoveryEnabled">
<div ng-if="app.showLogin">
<a href="#" ng-click="app.passwordRecoveryInfo()" sg-ripple-click="loginContent" class="password-lost-link"><var:string label:value="Password lost"/></a>
<!-- DOMAINS SELECT -->
<var:if condition="hasLoginDomains">
<div layout="row" layout-align="start end">
<md-icon>domain</md-icon>
<md-input-container class="md-flex">
<md-select class="md-flex" ng-model="app.creds.domain" label:placeholder="choose" ng-change="app.retrievePasswordRecoveryEnabled()">
<var:foreach list="loginDomains" item="item">
<md-option var:value="item">
<var:string value="item"/>
</md-option>
</var:foreach>
</md-select>
</md-input-container>
</div>
</var:if>
<div layout="row" layout-align="center center">
<md-switch class="md-accent md-hue-2"
ng-model="app.creds.rememberLogin"
label:arial-label="Remember username">
<var:string label:value="Remember username"/>
</md-switch>
</div>
<var:if condition="hasUrlCreateAccount">
<div layout="row" layout-align="center center">
<a var:href="urlCreateAccount" target="_blank" class="create-account-link"><var:string label:value="Create an account"/></a>
</div>
</var:if>
</div>
</div>
<!-- CONNECT BUTTON -->
<div layout="row" layout-align="space-between center" ng-if="!app.loginState">
<md-button class="md-icon-button"
label:aria-label="About"
ng-click="app.showAbout()">
<md-icon>info</md-icon>
</md-button>
<div>
<md-button class="md-fab md-accent md-hue-2" type="submit"
label:aria-label="Connect"
ng-if="!app.loginState"
ng-disabled="loginForm.$invalid"
sg-ripple-click="loginContent">
<md-icon>arrow_forward</md-icon>
<!-- Password recovery -->
<div layout="row" layout-align="center center" ng-if="app.passwordRecovery.passwordRecoveryEnabled">
<div ng-if="app.showLogin">
<a href="#" ng-click="app.passwordRecoveryInfo()" sg-ripple-click="loginContent" class="password-lost-link"><var:string label:value="Password lost"/></a>
</div>
</div>
<!-- CONNECT BUTTON -->
<div layout="row" layout-align="space-between center" ng-if="!app.loginState">
<md-button class="md-icon-button"
label:aria-label="About"
ng-click="app.showAbout()">
<md-icon>info</md-icon>
</md-button>
<div>
<md-button class="md-fab md-accent md-hue-2" type="submit"
label:aria-label="Connect"
ng-if="!app.loginState"
ng-disabled="loginForm.$invalid"
sg-ripple-click="loginContent">
<md-icon>arrow_forward</md-icon>
</md-button>
</div>
</div>
</div>
<sg-ripple class="md-default-theme md-accent md-bg"
ng-class="{ 'md-warn': app.loginState == 'error' }"><!-- ripple background --></sg-ripple>
<sg-ripple-content class="md-flex ng-hide"
layout="column" layout-align="center center" layout-fill="layout-fill"
ng-switch="app.loginState">
<sg-ripple class="md-default-theme md-accent md-bg"
ng-class="{ 'md-warn': app.loginState == 'error' }"><!-- ripple background --></sg-ripple>
<sg-ripple-content class="md-flex ng-hide"
layout="column" layout-align="center center" layout-fill="layout-fill"
ng-switch="app.loginState">
<!-- Authenticating -->
<div layout="column" layout-align="center center"
ng-switch-when="authenticating">
<md-progress-circular class="md-hue-1"
md-mode="indeterminate"
md-diameter="32"><!-- mailbox loading progress --></md-progress-circular>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding">
<var:string label:value="Authenticating"/>
<!-- Authenticating -->
<div layout="column" layout-align="center center"
ng-switch-when="authenticating">
<md-progress-circular class="md-hue-1"
md-mode="indeterminate"
md-diameter="32"><!-- mailbox loading progress --></md-progress-circular>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding">
<var:string label:value="Authenticating"/>
</div>
</div>
</div>
<var:if condition="isTotpEnabled">
<!-- TOTP Code -->
@ -163,7 +169,7 @@
<md-input-container class="md-block">
<label><var:string label:value="Verification Code"/></label>
<md-icon>lock</md-icon>
<input type="text" inputmode="numeric" autocomplete="off"
<input type="text" inputmode="numeric" autocomplete="off"
ng-pattern="app.verificationCodePattern"
ng-model="app.creds.verificationCode"
ng-required="app.loginState == 'totpcode'"
@ -258,126 +264,235 @@
</div>
</div>
<!-- Password policy: Grace period -->
<div layout="row" layout-align="center center" layout-fill="layout-fill"
ng-switch-when="passwordwillexpire">
<div layout="column" layout-align="center center" flex-xs="flex-xs" flex-gt-xs="50">
<md-icon class="md-accent md-hue-1 sg-icon--large">warning</md-icon>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding" ng-if="app.cn">
<var:string label:value="Welcome"/> {{app.cn}}
</div>
<div class="md-padding" layout="row" layout-align="start center">
<md-icon>priority_high</md-icon>
<div class="md-padding">{{app.errorMessage}}</div>
</div>
<div layout="row" layout-align="end center">
<md-button
ng-click="app.loginState = 'passwordexpired'"><var:string label:value="Change your Password"/></md-button>
<md-button
ng-click="app.continueLogin()"
sg-ripple-click="loginContent"><var:string label:value="Continue"/></md-button>
</div>
</div>
</div>
<!-- Password recovery -->
<var:if condition="hasPasswordRecovery">
<div layout="column" layout-align="center center"
ng-switch-when="passwordrecovery">
<md-icon class="md-accent md-hue-1 sg-icon--large">vpn_key</md-icon>
<div flex="100">
<div layout="row" layout-xs="column" class="md-padding" layout-align="center center">
<div ng-if="app.passwordRecovery.showLoader">
<md-progress-circular class="md-hue-1"
md-mode="indeterminate"
md-diameter="32"><!-- password recovery progress --></md-progress-circular>
</div>
<div ng-if="'SecretQuestion' === app.passwordRecovery.passwordRecoveryMode">
<div ng-if="!app.passwordRecovery.showLoader">
{{ app.passwordRecovery.passwordRecoveryQuestion }}
<md-input-container class="md-block">
<label><var:string label:value="Answer"/></label>
<input autocorrect="off" autocapitalize="off" type="text" ng-model="app.passwordRecovery.passwordRecoveryQuestionAnswer" />
</md-input-container>
</div>
</div>
<div ng-if="'SecondaryEmail' === app.passwordRecovery.passwordRecoveryMode">
<div ng-if="!app.passwordRecovery.showLoader">
{{ app.passwordRecovery.passwordRecoverySecondaryEmailText }}
</div>
</div>
<!-- Password policy: Grace period -->
<div layout="row" layout-align="center center" layout-fill="layout-fill"
ng-switch-when="passwordwillexpire">
<div layout="column" layout-align="center center" flex-xs="flex-xs" flex-gt-xs="50">
<md-icon class="md-accent md-hue-1 sg-icon--large">warning</md-icon>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding" ng-if="app.cn">
<var:string label:value="Welcome"/> {{app.cn}}
</div>
<div layout="row" layout-align="end center" ng-if="!app.passwordRecovery.showLoader">
<md-button ng-click="app.passwordRecoveryAbort()" type="button" >
<var:string label:value="Back"/>
</md-button>
<div ng-if="'SecretQuestion' === app.passwordRecovery.passwordRecoveryMode">
<md-button ng-click="app.passwordRecoveryCheck()" type="button" >
<var:string label:value="Next"/>
</md-button>
</div>
<div ng-if="'SecondaryEmail' === app.passwordRecovery.passwordRecoveryMode">
<md-button ng-click="app.passwordRecoveryEmail()" type="button" >
<var:string label:value="Next"/>
</md-button>
</div>
</div>
</div>
</div>
<div layout="column" layout-align="center center"
ng-switch-when="sendrecoverymail">
<md-icon class="md-accent md-hue-1 sg-icon--large">local_shipping</md-icon>
<div flex="100">
<div layout="row" layout-xs="column" class="md-padding">
<div ng-if="'SecondaryEmail' === app.passwordRecovery.passwordRecoveryMode">
<var:string label:value="A password reset link has been sent, please check your recovery e-mail mailbox and click on the link"/>
</div>
<div class="md-padding" layout="row" layout-align="start center">
<md-icon>priority_high</md-icon>
<div class="md-padding">{{app.errorMessage}}</div>
</div>
<div layout="row" layout-align="end center">
<md-button ng-click="app.passwordRecoveryAbort()" type="button" >
<var:string label:value="Back"/>
</md-button>
<md-button
ng-click="app.loginState = 'passwordexpired'"><var:string label:value="Change your Password"/></md-button>
<md-button
ng-click="app.continueLogin()"
sg-ripple-click="loginContent"><var:string label:value="Continue"/></md-button>
</div>
</div>
</div>
</var:if>
<!-- Logged in -->
<div layout="column" layout-align="center center"
ng-switch-when="logged">
<md-icon class="md-accent md-hue-1 sg-icon--large">done</md-icon>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding">
<var:string label:value="Welcome"/> {{app.cn}}
<!-- Password recovery -->
<var:if condition="hasPasswordRecovery">
<div layout="column" layout-align="center center"
ng-switch-when="passwordrecovery">
<md-icon class="md-accent md-hue-1 sg-icon--large">vpn_key</md-icon>
<div flex="100">
<div layout="row" layout-xs="column" class="md-padding" layout-align="center center">
<div ng-if="app.passwordRecovery.showLoader">
<md-progress-circular class="md-hue-1"
md-mode="indeterminate"
md-diameter="32"><!-- password recovery progress --></md-progress-circular>
</div>
<div ng-if="'SecretQuestion' === app.passwordRecovery.passwordRecoveryMode">
<div ng-if="!app.passwordRecovery.showLoader">
{{ app.passwordRecovery.passwordRecoveryQuestion }}
<md-input-container class="md-block">
<label><var:string label:value="Answer"/></label>
<input autocorrect="off" autocapitalize="off" type="text" ng-model="app.passwordRecovery.passwordRecoveryQuestionAnswer" />
</md-input-container>
</div>
</div>
<div ng-if="'SecondaryEmail' === app.passwordRecovery.passwordRecoveryMode">
<div ng-if="!app.passwordRecovery.showLoader">
{{ app.passwordRecovery.passwordRecoverySecondaryEmailText }}
</div>
</div>
</div>
<div layout="row" layout-align="end center" ng-if="!app.passwordRecovery.showLoader">
<md-button ng-click="app.passwordRecoveryAbort()" type="button" >
<var:string label:value="Back"/>
</md-button>
<div ng-if="'SecretQuestion' === app.passwordRecovery.passwordRecoveryMode">
<md-button ng-click="app.passwordRecoveryCheck()" type="button" >
<var:string label:value="Next"/>
</md-button>
</div>
<div ng-if="'SecondaryEmail' === app.passwordRecovery.passwordRecoveryMode">
<md-button ng-click="app.passwordRecoveryEmail()" type="button" >
<var:string label:value="Next"/>
</md-button>
</div>
</div>
</div>
</div>
<div layout="column" layout-align="center center"
ng-switch-when="sendrecoverymail">
<md-icon class="md-accent md-hue-1 sg-icon--large">local_shipping</md-icon>
<div flex="100">
<div layout="row" layout-xs="column" class="md-padding">
<div ng-if="'SecondaryEmail' === app.passwordRecovery.passwordRecoveryMode">
<var:string label:value="A password reset link has been sent, please check your recovery e-mail mailbox and click on the link"/>
</div>
</div>
<div layout="row" layout-align="end center">
<md-button ng-click="app.passwordRecoveryAbort()" type="button" >
<var:string label:value="Back"/>
</md-button>
</div>
</div>
</div>
</var:if>
<!-- Logged in -->
<div layout="column" layout-align="center center"
ng-switch-when="logged">
<md-icon class="md-accent md-hue-1 sg-icon--large">done</md-icon>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding">
<var:string label:value="Welcome"/> {{app.cn}}
</div>
</div>
</div>
<div layout="column" layout-align="center center"
ng-switch-when="message">
<md-icon class="md-accent md-hue-1 sg-icon--large">done</md-icon>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding">
{{app.errorMessage}}
<div layout="column" layout-align="center center"
ng-switch-when="message">
<md-icon class="md-accent md-hue-1 sg-icon--large">done</md-icon>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding">
{{app.errorMessage}}
</div>
<md-button
ng-click="app.continueLogin()"
sg-ripple-click="loginContent"><var:string label:value="Continue"/></md-button>
</div>
<md-button
ng-click="app.continueLogin()"
sg-ripple-click="loginContent"><var:string label:value="Continue"/></md-button>
</div>
<!-- Error -->
<div layout="column" layout-align="center center"
ng-switch-when="error">
<md-icon class="md-accent md-hue-1 sg-icon--large">error</md-icon>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding">
{{app.errorMessage}}
<!-- Error -->
<div layout="column" layout-align="center center"
ng-switch-when="error">
<md-icon class="md-accent md-hue-1 sg-icon--large">error</md-icon>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding">
{{app.errorMessage}}
</div>
<md-button
ng-click="app.restoreLogin()"
sg-ripple-click="loginContent"><var:string label:value="Retry"/></md-button>
</div>
<md-button
ng-click="app.restoreLogin()"
sg-ripple-click="loginContent"><var:string label:value="Retry"/></md-button>
</div>
</sg-ripple-content>
</form>
</sg-ripple-content>
</form>
</div>
</div>
</var:if>
<var:if condition="doPartialLogin">
<div id="login" class="sg-login-content md-padding">
<form name="loginForm" layout="column"
ng-cloak="ng-cloak"
ng-submit="app.loginName()">
<div ng-if="!app.loginState">
<md-input-container class="md-block">
<label><var:string label:value="Username"/></label>
<md-icon>person</md-icon>
<input autocorrect="off" autocapitalize="off" type="text" ng-model="app.creds.username" ng-required="true" ng-change="app.usernameChanged()" ng-blur="app.retrievePasswordRecoveryEnabled()" />
</md-input-container>
<!-- LANGUAGES SELECT -->
<div layout="row" layout-align="start end">
<md-icon>language</md-icon>
<md-input-container class="md-flex">
<label><var:string label:value="choose"/></label>
<md-select ng-model="app.creds.language"
var:placeholder="localizedLanguage"
ng-change="app.changeLanguage($event)">
<var:foreach list="languages" item="item">
<md-option var:value="item">
<var:string value="languageText"/>
</md-option>
</var:foreach>
</md-select>
</md-input-container>
</div>
<var:if condition="hasUrlCreateAccount">
<div layout="row" layout-align="center center">
<a var:href="urlCreateAccount" target="_blank" class="create-account-link"><var:string label:value="Create an account"/></a>
</div>
</var:if>
</div>
<!-- CONNECT BUTTON -->
<div layout="row" layout-align="space-between center" ng-if="!app.loginState">
<md-button class="md-icon-button"
label:aria-label="About"
ng-click="app.showAbout()">
<md-icon>info</md-icon>
</md-button>
<div>
<md-button class="md-fab md-accent md-hue-2" type="submit"
label:aria-label="Connect"
ng-if="!app.loginState"
ng-disabled="loginForm.$invalid"
sg-ripple-click="loginContent">
<md-icon>arrow_forward</md-icon>
</md-button>
</div>
</div>
<sg-ripple class="md-default-theme md-accent md-bg"
ng-class="{ 'md-warn': app.loginState == 'error' }"><!-- ripple background --></sg-ripple>
<sg-ripple-content class="md-flex ng-hide"
layout="column" layout-align="center center" layout-fill="layout-fill"
ng-switch="app.loginState">
<!-- Authenticating -->
<div layout="column" layout-align="center center"
ng-switch-when="authenticating">
<md-progress-circular class="md-hue-1"
md-mode="indeterminate"
md-diameter="32"><!-- mailbox loading progress --></md-progress-circular>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding">
<var:string label:value="Authenticating"/>
</div>
</div>
<!-- Logged in -->
<div layout="column" layout-align="center center"
ng-switch-when="logged">
<md-icon class="md-accent md-hue-1 sg-icon--large">done</md-icon>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding">
<var:string label:value="Welcome"/> {{app.cn}}
</div>
</div>
<div layout="column" layout-align="center center"
ng-switch-when="message">
<md-icon class="md-accent md-hue-1 sg-icon--large">done</md-icon>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding">
{{app.errorMessage}}
</div>
<md-button
ng-click="app.continueLogin()"
sg-ripple-click="loginContent"><var:string label:value="Continue"/></md-button>
</div>
<!-- Error -->
<div layout="column" layout-align="center center"
ng-switch-when="error">
<md-icon class="md-accent md-hue-1 sg-icon--large">error</md-icon>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding">
{{app.errorMessage}}
</div>
<md-button
ng-click="app.restoreLogin()"
sg-ripple-click="loginContent"><var:string label:value="Retry"/></md-button>
</div>
</sg-ripple-content>
</form>
</div>
</var:if>
</div>
</div>
</md-content>

File diff suppressed because one or more lines are too long

View File

@ -178,6 +178,45 @@
return d.promise;
}, // login: function(data) { ...
loginName: function(data) {
var d = $q.defer(),
username = data.username,
language;
if (data.language && data.language != 'WONoSelectionString') {
language = data.language;
}
$http({
method: 'POST',
url: '/SOGo/connectName?userName='+username,
// data: JSON.stringify({userName: username}),
data: {userName: username},
// headers: {
// //'Content-Type': undefined
// //'Content-Type': "application/x-www-form-urlencoded"
// 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8'
// }
}).then(function(response) {
var data = response.data;
// Make sure browser's cookies are enabled
if (navigator && !navigator.cookieEnabled) {
d.reject({error: l('cookiesNotEnabled')});
}
else {
if(data.redirect) {
//Redirection in case of openID
d.resolve({ url: data.redirect });
}
}
}, function(error) {
var response, perr, data = error.data;
d.reject(response);
});
return d.promise;
},
changePassword: function(userName, domain, newPassword, oldPassword, token) {
var d = $q.defer(),
xsrfCookie = $cookies.get('XSRF-TOKEN');

File diff suppressed because one or more lines are too long

View File

@ -210,9 +210,7 @@
}
url = url.join('/');
popupWindow = $window.open(url, wId,
["width=680",
"height=520",
"resizable=1",
["resizable=1",
"scrollbars=1",
"toolbar=0",
"location=0",

File diff suppressed because one or more lines are too long

View File

@ -20,6 +20,8 @@
domain: null,
rememberLogin: angular.isDefined($window.cookieUsername) && $window.cookieUsername.length > 0
};
if($window.loginHint)
this.creds.username = $window.loginHint;
// Send selected language only if user has changed it
if (/\blanguage=/.test($window.location.search))
this.creds.language = $window.language;
@ -164,6 +166,64 @@
return false;
};
this.loginName = function() {
vm.loginState = 'authenticating';
Authentication.loginName(vm.creds)
.then(function(data) {
vm.loginState = 'logged';
vm.cn = data.cn;
vm.url = data.url;
// Let the user see the succesfull message before reloading the page
$timeout(function() {
vm.continueLogin();
}, 1000);
}, function(msg) {
vm.loginState = 'error';
if (msg.error) {
vm.errorMessage = msg.error;
}
else if (msg.grace > 0) {
// Password is expired, grace logins limit is not yet reached
vm.loginState = 'passwordwillexpire';
vm.cn = msg.cn;
vm.url = msg.url;
vm.errorMessage = l('You have %{0} logins remaining before your account is locked. Please change your password in the preference dialog.', msg.grace);
}
else if (msg.expire > 0) {
// Password will soon expire
var value, string;
if (msg.expire > 86400) {
value = Math.round(msg.expire/86400);
string = l("days");
}
else if (msg.expire > 3600) {
value = Math.round(msg.expire/3600);
string = l("hours");
}
else if (msg.expire > 60) {
value = Math.round(msg.expire/60);
string = l("minutes");
}
else {
value = msg.expire;
string = l("seconds");
}
vm.loginState = 'passwordwillexpire';
vm.cn = msg.cn;
vm.url = msg.url;
vm.errorMessage = l('Your password is going to expire in %{0} %{1}.', value, string);
}
else if (msg.passwordexpired) {
vm.loginState = 'passwordchange';
vm.url = msg.url;
}
});
return false;
};
this.restoreLogin = function () {
if ('SecretQuestion' === vm.passwordRecovery.passwordRecoveryMode) {
rippleDo('loginContent');