61

I am writing a Asp.net MVC 2 application that uses Forms Authentication and currently I am having a problem with our iPhone application in regards to the authentication/login over the web. We have developed a simple iPhone app that uses the the UIWebView control. At this stage, all the app does is navigate to our Asp.Net website. Simple, right? The problem is, that the user cannot get past the login page. The repro steps are:

  • Open iPhone app.
  • The app navigates to the home page.
  • the user is not authenticated, so they are redirected to the login screen/page
  • The user enters the correct user name and password. clicks submit.
  • on the server side, the user is authenticated and a cookie is generated and sent to the client using FormsAuthentication.GetAuthCookie.
  • The server sends are redirect to send the user to the correct home page.

But the user is then redirected BACK to the login screen!

I've done some extensive debugging on this and what I do know is:

The cookie is being sent to the client, and the client is storing the cookie. Verified this in the iPhone debugger and also by using Javsascript to display cookie data on the page. The cookie is being sent back to the server. Verified this in the Visual Studio debugger. It is the correct cookie (it's the same one that was set). The property User.Identity.IsAuthenticated returns false for some reason, even though the auth cookie is contained in the Request object. I have verified that the iPhone app is setup to accept cookies, and they are on the client.

Here is the funny thing: It works fine if you open the Safari browser on the iPhone and go to our site directly.

It has the same behaviour on the iPad too in that it doesn't get past the login screen. This repros on the emulators, and on devices.

This same web site has been tested with IE 7-8, Safari (for Windows), Blackberry, IEMobile 6.5, Phone 7 and it works find. The only circumstance that it doesn't work on is the UIWebView in the iPhone app.

jrummell
  • 41,300
  • 17
  • 110
  • 165
Brian Y
  • 1,138
  • 1
  • 10
  • 11
  • Have you managed to find a solution to this problem? I'm facing the same problem... :( – William Niu Dec 13 '10 at 05:48
  • Yes, we did manage to find a solution. I'll ask the developer who solved the problem to post his solution, as he understands it better than me. – Brian Y Dec 14 '10 at 18:40
  • Please provide the solution as I'm facing the same issue as well. Thank you. – Neal Apr 16 '11 at 16:49

6 Answers6

44

I had exactly the same problem, but with another device (NokiaN8), and also traced the problem back to the User-Agent.

IIS uses regular expressions to match against the User-Agent string. The root of the problem was that it didn't have any matching regular expressions for the specific device, and ended up in one of the lowest levels of match, where the Default properties were used. The default properties said that the browser didn't support cookies.

Solution:

  1. Add a folder in your web project named App_Browsers (right-click the project, choose: Add > Add ASP.NET Folder > App_Browsers).
  2. Add a file in that folder (right-click, choose: Add > New Item). The file can have any name, but must have the .browser ending.
  3. Add a good matching expression and the correct capabilities (or add changes to the Default).

Two examples:

<browsers>
  <browser id="NokiaN8" parentID="Mozilla">
    <identification>
      <userAgent match="NokiaN8" />
    </identification>
    <capabilities>
      <capability name="browser" value="NokiaN8" />
      <capability name="cookies" value="true" /> 
    </capabilities> 
  </browser> 
</browsers>

Or change the default:

<browsers>
  <browser refID="Default"> 
    <capabilities> 
      <capability name="cookies" value="true" /> 
    </capabilities>
  </browser>
</browsers>

More info: Browser Definition File Schema

balexandre
  • 69,002
  • 44
  • 219
  • 321
Hunterwood
  • 441
  • 4
  • 3
  • 1
    The odd thing is that if we changed the User-Agent to "blah blah blah" it actually worked. It was just the specific User-Agent that the built-in web browser view used in the iPhone that was failing. It seemed that the IIS server was not just defaulting to an unknown browser, but it was actually choking on the User-Agent string. – Brian Y Jan 28 '11 at 19:24
  • 1
    We discovered that the problem was that in ASP.Net 4.0, the parsing of the user-agent was done is such a way that the UIWebView control was being identified as the "Mozilla" browser, since there was no references to Safari in the User-Agent. When I looked at the various .browser files on the server, I found that the file that contains the match for Mozilla browsers (generic.browser) contained the line . Therefore, the ASP.Net server was considering that all Mozilla browsers did not support cookies. – Brian Y Mar 18 '11 at 21:51
  • 1
    Can someone please provide the exact browser definition I need to add to App_Browsers so I can get my UIWebView to work under asp.net 4? No one can login to my site etc. while this is broken. Thank you. – Neal Apr 16 '11 at 16:27
  • @Neal, I posted a solution for you. – Brian Y Apr 20 '11 at 23:49
42

The solution we found was to create a file (generic.browser) and include this xml to tell the web server that "Mozilla" and the Default browser settings should all support cookies.

<browser refID="Mozilla" >
    <capabilities>
        <capability name="cookies"  value="true" />
    </capabilities>
</browser>
Scott Hanselman
  • 17,492
  • 6
  • 70
  • 89
Brian Y
  • 1,138
  • 1
  • 10
  • 11
  • Thank you for the sample. I was able to use this and compare to a few other files in the config folder and get what I needed. I'm not sure if you can use the same name as an existing browser definition so I created generic2.browser just to be safe. Thanks again!!! – Neal Apr 21 '11 at 13:53
  • Adding the default entry with cookies=true fixed the issue for me. – Sam Jun 13 '12 at 03:32
  • Man!! this fixed the problem! Safari browser was not storing forms authentication persistent cookies at all. Please please explain: Solution 1 & 2 of your own post here did not work - http://www.hanselman.com/blog/FormsAuthenticationOnASPNETSitesWithTheGoogleChromeBrowserOnIOS.aspx . But how come "Mozilla" work? Because Mozilla is not Apple right? – Gautam Jain Aug 12 '12 at 15:51
  • In continuation to by previous comment, it works after closing & opening the browser so it is persistent. But it takes timeout as 30 minutes only although I have set timeout to 48 hours. Any thoughts please? – Gautam Jain Aug 13 '12 at 06:00
  • FYI: Private Browsing mode will also cause these symptoms – Andrew Bullock Sep 11 '12 at 08:43
  • This is really confusing, since i had to implement this for a web application just on one webserver, the other one worked well using the exact same application files. Does anyone know, if IIS settings can influence the behaviour in this regard? – Kai Hartmann Nov 20 '13 at 15:31
18

This is fixed in ASP.NET 4.5 and all browsers are assumed to support cookies, so the extra .browser file won't be needed.

Scott Hanselman
  • 17,492
  • 6
  • 70
  • 89
  • 2
    I've tried all the approaches on this page, AND I'm running in .NET 4.5 and it still doesn't work. http://stackoverflow.com/questions/24781648/cookies-not-saved-between-browser-sessions-on-ios – ganders Aug 05 '14 at 20:31
5

From the research I did, the reason why you can't set the User-Agent is that the UIWebView is setting the User-Agent value just before it sends out the request, that is, after you've made your request from your code.

The trick to get around this problem is to use something called "method swizzling", an advanced and potentially dangerous Objective-C concept that swaps out a standard method with one you provide. The end result is that when your request is sent out and the framework code adds the User-Agent it will be fooled into using the method you provided.

The following explains what I did to implement this but I am no Objective-C expert and would suggest to you to do some research to familiarize yourself with the technique. In particular, there was a link out there explaining better than me what is going on here, but at the moment I can't find it.

1) Add a category on NSObject to allow swizzling.

@interface NSObject (Swizzle)

+ (BOOL) swizzleMethod:(SEL)origSelector withMethod:(SEL)newSelector;

@end

@implementation NSObject (Swizzle)


+ (BOOL) swizzleMethod:(SEL) origSelector withMethod:(SEL)newSelector
{
    Method origMethod= class_getInstanceMethod(self, origSelector);
    Method newMethod= class_getInstanceMethod(self, newSelector);

    if (origMethod && newMethod)
    {
        if (class_addMethod(self, origSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)))
        {
            class_replaceMethod(self, newSelector, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
        }
        else {
            method_exchangeImplementations(origMethod, newMethod);
        }
        return YES;
    }
    return NO;
}
@end

2) Subclass NSMutableURLRequest to allow the swizzle:

@interface NSMutableURLRequest (MyMutableURLRequest)

+ (void) setupUserAgentOverwrite;

@end
@implementation NSMutableURLRequest (MyMutableURLRequest)

- (void) newSetValue:(NSString*)value forHTTPHeaderField:(NSString*)field
{
    if ([field isEqualToString:@"User-Agent"])
    {
        value = USER_AGENT;  // ie, the value I want to use.
    }
    [self newSetValue:value forHTTPHeaderField:field];
}
+ (void) setupUserAgentOverwrite
{
    [self swizzleMethod:@selector(setValue:forHTTPHeaderField:) 
             withMethod:@selector(newSetValue:forHTTPHeaderField:)];

}

@end

3) Call the static method to swap out the method. I made this call in didFinishLaunchingWithOptions:

// Need to call this method so that User-Agent get updated correctly:
[NSMutableURLRequest setupUserAgentOverwrite];

4) And then used it like this. (The connection delegate saves the data in a mutable array and then manually sets the UIWebView using its loadData method when it finishes loading).

- (void)loadWithURLString:(NSString*)urlString
{
    NSURL *url = [NSURL URLWithString:urlString];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
_connection = [NSURLConnection connectionWithRequest:request delegate:self];
[_connection start];
}
Jack
  • 101
  • 2
  • 1
    It fixed the problem! Thanks, mate! – William Niu Dec 15 '10 at 23:52
  • To add to the answer, I discovered that ASP.NET is not happy about the string "Mozilla" in user agent, but it is okay when that is followed by the string "Version/x.x Safari". And that's why the user agent string provided by UIWebView failed, while the one provided by the mobile Safari go through. Silly ASP.NET... – William Niu Dec 16 '10 at 07:33
  • 1
    William, you are correct about the issue with "mozilla" in the user-agent. If you look at the accepted answer, there is information about how to address this in the server side. We were able to roll back all our iPhone code that involved hacking the user-agent because the server would now recognize the cookies. – Brian Y Mar 18 '11 at 21:54
  • see http://stackoverflow.com/questions/478387/change-user-agent-in-uiwebview-iphone-sdk for an easier way to change the User Agent – JoelFan Dec 10 '12 at 02:10
0

The reason of this happening apparently has to do with the fact that if the user-agent is not known then the browser is assumed to not accept cookies (as others have answered), and instead IIS puts the ASPXAUTH value in the URL.

However the MVC routing system apparently missed that possibility, which is clearly a bug, and therefore it is getting messed up.

While adding the .browser with a custom user-agent solves the problem, it does not guarantee that other user-agents will also be solved, and in fact I have found the K9 Browser for the android also has this problem, and as such it is only a solution if one has a logging system such as elmeh to track down such errors.

On the other hand adding a default brings up the question if it is true that all browsers accept cookies, which is apparently the reason why IIS does not assume so.

However besides adding explictly the user-agents one can add in the global.asax RegiterRoutes() method an explicit handler to ignore it, as follows:

         routes.MapRoute(
            "CookieLess", // Route name
            "(F({Cookie}))/{controller}/{action}/{id}", // URL with parameters
            new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
        );

However in this case one will have to copy all route entries to match with the cookie-less situation, unless one is about to write a custom route handler.

Or we can use the above cookie-less route to send the user to an error page explaining that his browser is not supported on the moment, and send an alert to the web-master with the user-agent to handle it.

yoel halb
  • 10,405
  • 3
  • 48
  • 46
0
  1. Have you specified a DestinationPageUrl in markup?

  2. Have you specified the defaultURL in web.config?

Example web.config

<authentication mode="Forms">
     <forms loginUrl="~/Login.aspx" defaultUrl="~/CustomerArea/Default.aspx"/>
</authentication>

Example DestinationPageUrl

 <asp:Login ID="Login" runat="server" DestinationPageUrl="~/Secret/Default.aspx" />

Lastly have you looked in the cookie jar and seen if your session cookie actually exists?

Where are an UIWebView's cookies stored?

Community
  • 1
  • 1
Maxim Gershkovich
  • 42,124
  • 39
  • 134
  • 227
  • Thanks for the suggestions in ASP.NET. I'll give them a try. Re. cookies, yes, they are correctly set and sent as described in the problem. – William Niu Dec 14 '10 at 07:06
  • I don't know specifically where the UIWebView is storing the cookies. I do know that each application on iOS has it's own cache and cookie storate. Yes, I have verified that the cookies are being stored on the device, and they are being sent back to the server as well. – Brian Y Dec 14 '10 at 18:38