Ben Copsey

Cached credentials will not be used if different ones are set on the request

Tweak to finding NTLM proxy credentials
@@ -939,41 +939,45 @@ static NSOperationQueue *sharedQueue = nil; @@ -939,41 +939,45 @@ static NSOperationQueue *sharedQueue = nil;
939 if (![self shouldPresentCredentialsBeforeChallenge]) { 939 if (![self shouldPresentCredentialsBeforeChallenge]) {
940 return; 940 return;
941 } 941 }
942 - 942 +
943 - // First, see if we have any credentials we can use in the session store  
944 NSDictionary *credentials = nil; 943 NSDictionary *credentials = nil;
945 - if ([self useSessionPersistence]) { 944 +
946 - credentials = [self findSessionAuthenticationCredentials]; 945 + // Do we already have an auth header?
947 - } 946 + if (![[self requestHeaders] objectForKey:@"Authorization"]) {
948 - 947 +
949 - 948 + // If we have basic authentication explicitly set and a username and password set on the request, add a basic auth header
950 - // Are any credentials set on this request that might be used for basic authentication? 949 + if ([self username] && [self password] && [[self authenticationScheme] isEqualToString:(NSString *)kCFHTTPAuthenticationSchemeBasic]) {
951 - if ([self username] && [self password] && ![self domain]) {  
952 -  
953 - // If we know this request should use Basic auth, we'll add an Authorization header with basic credentials  
954 - if ([[self authenticationScheme] isEqualToString:(NSString *)kCFHTTPAuthenticationSchemeBasic]) {  
955 [self addBasicAuthenticationHeaderWithUsername:[self username] andPassword:[self password]]; 950 [self addBasicAuthenticationHeaderWithUsername:[self username] andPassword:[self password]];
956 - } 951 +
957 - }  
958 -  
959 - if (credentials && ![[self requestHeaders] objectForKey:@"Authorization"]) {  
960 -  
961 - // When the Authentication key is set, the credentials were stored after an authentication challenge, so we can let CFNetwork apply them  
962 - // (credentials for Digest and NTLM will always be stored like this)  
963 - if ([credentials objectForKey:@"Authentication"]) {  
964 -  
965 - // If we've already talked to this server and have valid credentials, let's apply them to the request  
966 - if (!CFHTTPMessageApplyCredentialDictionary(request, (CFHTTPAuthenticationRef)[credentials objectForKey:@"Authentication"], (CFDictionaryRef)[credentials objectForKey:@"Credentials"], NULL)) {  
967 - [[self class] removeAuthenticationCredentialsFromSessionStore:[credentials objectForKey:@"Credentials"]];  
968 - }  
969 -  
970 - // If the Authentication key is not set, these credentials were stored after a username and password set on a previous request passed basic authentication  
971 - // When this happens, we'll need to create the Authorization header ourselves  
972 } else { 952 } else {
973 - NSDictionary *usernameAndPassword = [credentials objectForKey:@"Credentials"]; 953 +
974 - [self addBasicAuthenticationHeaderWithUsername:[usernameAndPassword objectForKey:(NSString *)kCFHTTPAuthenticationUsername] andPassword:[usernameAndPassword objectForKey:(NSString *)kCFHTTPAuthenticationPassword]]; 954 + // See if we have any cached credentials we can use in the session store
  955 + if ([self useSessionPersistence]) {
  956 + credentials = [self findSessionAuthenticationCredentials];
  957 +
  958 + if (credentials) {
  959 +
  960 + // When the Authentication key is set, the credentials were stored after an authentication challenge, so we can let CFNetwork apply them
  961 + // (credentials for Digest and NTLM will always be stored like this)
  962 + if ([credentials objectForKey:@"Authentication"]) {
  963 +
  964 + // If we've already talked to this server and have valid credentials, let's apply them to the request
  965 + if (!CFHTTPMessageApplyCredentialDictionary(request, (CFHTTPAuthenticationRef)[credentials objectForKey:@"Authentication"], (CFDictionaryRef)[credentials objectForKey:@"Credentials"], NULL)) {
  966 + [[self class] removeAuthenticationCredentialsFromSessionStore:[credentials objectForKey:@"Credentials"]];
  967 + }
  968 +
  969 + // If the Authentication key is not set, these credentials were stored after a username and password set on a previous request passed basic authentication
  970 + // When this happens, we'll need to create the Authorization header ourselves
  971 + } else {
  972 + NSDictionary *usernameAndPassword = [credentials objectForKey:@"Credentials"];
  973 + [self addBasicAuthenticationHeaderWithUsername:[usernameAndPassword objectForKey:(NSString *)kCFHTTPAuthenticationUsername] andPassword:[usernameAndPassword objectForKey:(NSString *)kCFHTTPAuthenticationPassword]];
  974 + }
  975 + }
  976 + }
975 } 977 }
976 } 978 }
  979 +
  980 + // Apply proxy authentication credentials
977 if ([self useSessionPersistence]) { 981 if ([self useSessionPersistence]) {
978 credentials = [self findSessionProxyAuthenticationCredentials]; 982 credentials = [self findSessionProxyAuthenticationCredentials];
979 if (credentials) { 983 if (credentials) {
@@ -2368,17 +2372,24 @@ static NSOperationQueue *sharedQueue = nil; @@ -2368,17 +2372,24 @@ static NSOperationQueue *sharedQueue = nil;
2368 NSString *user = nil; 2372 NSString *user = nil;
2369 NSString *pass = nil; 2373 NSString *pass = nil;
2370 2374
2371 - 2375 + ASIHTTPRequest *theRequest = [self mainRequest];
2372 // If this is a HEAD request generated by an ASINetworkQueue, we'll try to use the details from the main request 2376 // If this is a HEAD request generated by an ASINetworkQueue, we'll try to use the details from the main request
2373 - if ([self mainRequest] && [[self mainRequest] proxyUsername] && [[self mainRequest] proxyPassword]) { 2377 + if ([theRequest proxyUsername] && [theRequest proxyPassword]) {
2374 - user = [[self mainRequest] proxyUsername]; 2378 + user = [theRequest proxyUsername];
2375 - pass = [[self mainRequest] proxyPassword]; 2379 + pass = [theRequest proxyPassword];
2376 2380
2377 - // Let's try to use the ones set in this object 2381 + // Let's try to use the ones set in this object
2378 } else if ([self proxyUsername] && [self proxyPassword]) { 2382 } else if ([self proxyUsername] && [self proxyPassword]) {
2379 user = [self proxyUsername]; 2383 user = [self proxyUsername];
2380 pass = [self proxyPassword]; 2384 pass = [self proxyPassword];
2381 - } 2385 + }
  2386 +
  2387 + // When we connect to a website using NTLM via a proxy, we will use the main credentials
  2388 + if ((!user || !pass) && [self proxyAuthenticationScheme] == (NSString *)kCFHTTPAuthenticationSchemeNTLM) {
  2389 + user = [self username];
  2390 + pass = [self password];
  2391 + }
  2392 +
2382 2393
2383 2394
2384 // Ok, that didn't work, let's try the keychain 2395 // Ok, that didn't work, let's try the keychain
@@ -2397,13 +2408,21 @@ static NSOperationQueue *sharedQueue = nil; @@ -2397,13 +2408,21 @@ static NSOperationQueue *sharedQueue = nil;
2397 2408
2398 NSString *ntlmDomain = [self proxyDomain]; 2409 NSString *ntlmDomain = [self proxyDomain];
2399 2410
2400 - // If we have no domain yet, let's try to extract it from the username 2411 + // If we have no domain yet
2401 if (!ntlmDomain || [ntlmDomain length] == 0) { 2412 if (!ntlmDomain || [ntlmDomain length] == 0) {
2402 - ntlmDomain = @""; 2413 +
  2414 + // Let's try to extract it from the username
2403 NSArray* ntlmComponents = [user componentsSeparatedByString:@"\\"]; 2415 NSArray* ntlmComponents = [user componentsSeparatedByString:@"\\"];
2404 if ([ntlmComponents count] == 2) { 2416 if ([ntlmComponents count] == 2) {
2405 ntlmDomain = [ntlmComponents objectAtIndex:0]; 2417 ntlmDomain = [ntlmComponents objectAtIndex:0];
2406 user = [ntlmComponents objectAtIndex:1]; 2418 user = [ntlmComponents objectAtIndex:1];
  2419 +
  2420 + // If we are connecting to a website using NTLM, but we are connecting via a proxy, the string we need may be in the domain property
  2421 + } else {
  2422 + ntlmDomain = [self domain];
  2423 + }
  2424 + if (!ntlmDomain) {
  2425 + ntlmDomain = @"";
2407 } 2426 }
2408 } 2427 }
2409 [newCredentials setObject:ntlmDomain forKey:(NSString *)kCFHTTPAuthenticationAccountDomain]; 2428 [newCredentials setObject:ntlmDomain forKey:(NSString *)kCFHTTPAuthenticationAccountDomain];
@@ -2789,6 +2808,7 @@ static NSOperationQueue *sharedQueue = nil; @@ -2789,6 +2808,7 @@ static NSOperationQueue *sharedQueue = nil;
2789 return; 2808 return;
2790 } 2809 }
2791 2810
  2811 + // Do we actually need to authenticate with a proxy?
2792 if ([self authenticationNeeded] == ASIProxyAuthenticationNeeded) { 2812 if ([self authenticationNeeded] == ASIProxyAuthenticationNeeded) {
2793 [self attemptToApplyProxyCredentialsAndResume]; 2813 [self attemptToApplyProxyCredentialsAndResume];
2794 return; 2814 return;
@@ -3971,31 +3991,59 @@ static NSOperationQueue *sharedQueue = nil; @@ -3971,31 +3991,59 @@ static NSOperationQueue *sharedQueue = nil;
3971 { 3991 {
3972 [sessionCredentialsLock lock]; 3992 [sessionCredentialsLock lock];
3973 NSMutableArray *sessionCredentialsList = [[self class] sessionCredentialsStore]; 3993 NSMutableArray *sessionCredentialsList = [[self class] sessionCredentialsStore];
3974 - // Find an exact match (same url)  
3975 - for (NSDictionary *theCredentials in sessionCredentialsList) {  
3976 - if ([(NSURL*)[theCredentials objectForKey:@"URL"] isEqual:[self url]]) {  
3977 - // /Just a sanity check to ensure we never choose credentials from a different realm. Can't really do more than that, as either this request or the stored credentials may not have a realm when the other does  
3978 - if (![self responseStatusCode] || (![theCredentials objectForKey:@"AuthenticationRealm"] || [[theCredentials objectForKey:@"AuthenticationRealm"] isEqualToString:[self authenticationRealm]])) {  
3979 - [sessionCredentialsLock unlock];  
3980 - return theCredentials;  
3981 - }  
3982 - }  
3983 - }  
3984 - // Find a rough match (same host, port, scheme)  
3985 NSURL *requestURL = [self url]; 3994 NSURL *requestURL = [self url];
  3995 +
  3996 + BOOL haveFoundExactMatch;
  3997 + NSDictionary *closeMatch = nil;
  3998 +
  3999 + // Loop through all the cached credentials we have, looking for the best match for this request
3986 for (NSDictionary *theCredentials in sessionCredentialsList) { 4000 for (NSDictionary *theCredentials in sessionCredentialsList) {
3987 - NSURL *theURL = [theCredentials objectForKey:@"URL"];  
3988 4001
3989 - // Port can be nil! 4002 + haveFoundExactMatch = NO;
3990 - if ([[theURL host] isEqualToString:[requestURL host]] && ([theURL port] == [requestURL port] || ([requestURL port] && [[theURL port] isEqualToNumber:[requestURL port]])) && [[theURL scheme] isEqualToString:[requestURL scheme]]) { 4003 + NSURL *cachedCredentialsURL = [theCredentials objectForKey:@"URL"];
3991 - if (![self responseStatusCode] || (![theCredentials objectForKey:@"AuthenticationRealm"] || [[theCredentials objectForKey:@"AuthenticationRealm"] isEqualToString:[self authenticationRealm]])) { 4004 +
3992 - [sessionCredentialsLock unlock]; 4005 + // Find an exact match (same url)
3993 - return theCredentials; 4006 + if ([cachedCredentialsURL isEqual:[self url]]) {
  4007 + haveFoundExactMatch = YES;
  4008 +
  4009 + // This is not an exact match for the url, and we already have a close match we can use
  4010 + } else if (closeMatch) {
  4011 + continue;
  4012 +
  4013 + // Find a close match (same host, scheme and port)
  4014 + } else if ([[cachedCredentialsURL host] isEqualToString:[requestURL host]] && ([cachedCredentialsURL port] == [requestURL port] || ([requestURL port] && [[cachedCredentialsURL port] isEqualToNumber:[requestURL port]])) && [[cachedCredentialsURL scheme] isEqualToString:[requestURL scheme]]) {
  4015 + } else {
  4016 + continue;
  4017 + }
  4018 +
  4019 + // Just a sanity check to ensure we never choose credentials from a different realm. Can't really do more than that, as either this request or the stored credentials may not have a realm when the other does
  4020 + if ([self authenticationRealm] && ([theCredentials objectForKey:@"AuthenticationRealm"] && ![[theCredentials objectForKey:@"AuthenticationRealm"] isEqualToString:[self authenticationRealm]])) {
  4021 + continue;
  4022 + }
  4023 +
  4024 + // If we have a username and password set on the request, check that they are the same as the cached ones
  4025 + if ([self username] && [self password]) {
  4026 + NSDictionary *usernameAndPassword = [theCredentials objectForKey:@"Credentials"];
  4027 + NSString *storedUsername = [usernameAndPassword objectForKey:(NSString *)kCFHTTPAuthenticationUsername];
  4028 + NSString *storedPassword = [usernameAndPassword objectForKey:(NSString *)kCFHTTPAuthenticationUsername];
  4029 + if (![storedUsername isEqualToString:[self username]] || ![storedPassword isEqualToString:[self password]]) {
  4030 + continue;
3994 } 4031 }
3995 } 4032 }
  4033 +
  4034 + // If we have an exact match for the url, use those credentials
  4035 + if (haveFoundExactMatch) {
  4036 + [sessionCredentialsLock unlock];
  4037 + return theCredentials;
  4038 + }
  4039 +
  4040 + // We have no exact match, let's remember that we have a good match for this server, and we'll use it at the end if we don't find an exact match
  4041 + closeMatch = theCredentials;
3996 } 4042 }
3997 [sessionCredentialsLock unlock]; 4043 [sessionCredentialsLock unlock];
3998 - return nil; 4044 +
  4045 + // Return credentials that matched on host, port and scheme, or nil if we didn't find any
  4046 + return closeMatch;
3999 } 4047 }
4000 4048
4001 #pragma mark keychain storage 4049 #pragma mark keychain storage
@@ -1036,10 +1036,10 @@ @@ -1036,10 +1036,10 @@
1036 1036
1037 - (void)testBasicAuthentication 1037 - (void)testBasicAuthentication
1038 { 1038 {
1039 - [ASIHTTPRequest removeCredentialsForHost:@"allseeing-i.com" port:0 protocol:@"http" realm:@"SECRET_STUFF"]; 1039 + [ASIHTTPRequest removeCredentialsForHost:@"asi" port:0 protocol:@"http" realm:@"SECRET_STUFF"];
1040 [ASIHTTPRequest clearSession]; 1040 [ASIHTTPRequest clearSession];
1041 1041
1042 - NSURL *url = [NSURL URLWithString:@"http://allseeing-i.com/ASIHTTPRequest/tests/basic-authentication"]; 1042 + NSURL *url = [NSURL URLWithString:@"http://asi/ASIHTTPRequest/tests/basic-authentication"];
1043 ASIHTTPRequest *request; 1043 ASIHTTPRequest *request;
1044 BOOL success; 1044 BOOL success;
1045 NSError *err; 1045 NSError *err;
@@ -1087,6 +1087,16 @@ @@ -1087,6 +1087,16 @@
1087 err = [request error]; 1087 err = [request error];
1088 GHAssertNil(err,@"Failed to reuse credentials"); 1088 GHAssertNil(err,@"Failed to reuse credentials");
1089 1089
  1090 + // Ensure new credentials are used in place of those in the session
  1091 + request = [[[ASIHTTPRequest alloc] initWithURL:[NSURL URLWithString:@"http://asi/ASIHTTPRequest/tests/basic-authentication-new-credentials"]] autorelease];
  1092 + [request setUsername:@"secret_username_2"];
  1093 + [request setPassword:@"secret_password_2"];
  1094 + [request setUseSessionPersistence:YES];
  1095 + [request setUseKeychainPersistence:NO];
  1096 + [request startSynchronous];
  1097 + err = [request error];
  1098 + GHAssertNil(err,@"Failed to reuse credentials");
  1099 +
1090 [ASIHTTPRequest clearSession]; 1100 [ASIHTTPRequest clearSession];
1091 1101
1092 // Ensure credentials stored in the session were wiped 1102 // Ensure credentials stored in the session were wiped
@@ -1103,7 +1113,7 @@ @@ -1103,7 +1113,7 @@
1103 err = [request error]; 1113 err = [request error];
1104 GHAssertNil(err,@"Failed to use stored credentials"); 1114 GHAssertNil(err,@"Failed to use stored credentials");
1105 1115
1106 - [ASIHTTPRequest removeCredentialsForHost:@"allseeing-i.com" port:0 protocol:@"http" realm:@"SECRET_STUFF"]; 1116 + [ASIHTTPRequest removeCredentialsForHost:@"asi" port:0 protocol:@"http" realm:@"SECRET_STUFF"];
1107 1117
1108 // Ensure credentials stored in the keychain were wiped 1118 // Ensure credentials stored in the keychain were wiped
1109 request = [[[ASIHTTPRequest alloc] initWithURL:url] autorelease]; 1119 request = [[[ASIHTTPRequest alloc] initWithURL:url] autorelease];