Like plenty of other people before me, I have some web content that I want to display in my app. My site is an ASP.NET-based site developed in Microsoft Visual Web Developer, and it uses Ajax Toolkit among other nifty addons. I can open up the site in my iPhone's mobile Safari browser, and I've even thrown together some mobile-friendly pages.
I (or more accurately, my boss) want an iPhone app which stores credentials used to access my site and sends them automatically upon opening the app. Here's the problem:
My site doesn't load in a UIWebView. It doesn't run into any of the delegate methods either (finish loading, failed to load, etc). It simply starts up the connection request and sits there. The site does work in Safari on the same device I'm using for testing, so I figure the problem has something to do with UIWebKit not having some tools built in that Safari does.
What (specifically) is causing my site not to load in UIWebKit, when it is loading correctly in Safari?
I will post the methods I have set up to catch any signs of life from my call to
[myWebView loadRequest:myRequest];
None of the following methods are being called when I run my app and reach the loadRequest call above.
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
NSLog(#"WillSendForAuthenticationChallenge");
}
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
NSLog(#"Webview failed to load.");
if (error.code == NSURLErrorCancelled) return; // this is Error -999, called when the user forcibly ends the connection by starting a new connection.
UIAlertView *failure = [[UIAlertView alloc] initWithTitle:#"Failed" message:error.localizedDescription delegate:self cancelButtonTitle:nil otherButtonTitles:#"OK", nil];
[failure show];
[spinner stopAnimating];
}
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSLog(#"Loading request: %#",request.URL);
return YES;
}
- (void)webViewDidFinishLoad:(UIWebView *)webview
{
NSLog(#"Webview finished loading.");
[spinner stopAnimating];
}
- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace {
NSLog(#"Can Authenticate: %#",[protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]);
return [protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust];
}
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
NSLog(#"Received an authentication challenge.");
[challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
}
Well, here's how I solved it. The UIWebView never calls any of the NSURLConnection methods, and it doesn't have any built-in security methods, so when the web view is about to load a page, I first send an NSURLConnection to the same address, which can then handle the authentication through methods like didReceiveAuthenticationChallenge. The variable "_sessionChecked" is a simple BOOL which keep track of whether or not the authenticating NSURLConnection has already been sent for this page. Once it has, I can simply load the page into the web view normally.
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSLog(#"Loading request: %#",request.URL);
if (_sessionChecked) {
NSLog(#"Session already checked.");
return YES;
}
NSLog(#"Will check session.");
NSMutableURLRequest *newReq = request.mutableCopy;
[newReq setValue:USRAGNT forHTTPHeaderField:#"User-Agent"];
NSURLConnection *conn = [NSURLConnection connectionWithRequest:newReq delegate:self];
if (conn == nil) {
NSLog(#"Cannot create connection.");
}
return NO;
}
The didReceiveResponse method is called when the NSURLConnection gets a response from the server. This response contains two important things: whether or not the connection was successfully authenticated with the credentials supplied by the NSURLRequest, and what url to be directed to if it was. Upon successful authentication, I send the new url to the web view to be loaded.
- (void) connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
NSLog(#"connection:didReceiveResponse");
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
_sessionChecked = YES;
NSString *newUrl = #"";
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response;
int status = [httpResponse statusCode];
if (status == 401 || status == 403) {
NSLog(#"Not authenticated. http status code: %d", status);
newUrl = #"";
[myWebView stopLoading];
[spinner stopAnimating];
UIAlertView *failed = [[UIAlertView alloc] initWithTitle:#"Authentication Failed" message:#"You do not have any credentials stored, or your stored credentials are not valid." delegate:self cancelButtonTitle:nil otherButtonTitles:#"OK", nil];
[failed show];
_sessionChecked = NO;
}
else {
NSLog(#"Authenticated. http status code: %d", status);
newUrl = [NSString stringWithFormat:#"%#", response.URL];
}
// cancel the connection. we got what we want from the response,
// no need to download the response data.
[connection cancel];
// start loading the new request in webView
NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL URLWithString:newUrl]];
[myWebView loadRequest:req];
}
}
Many thanks to Ardal Ahmet for this how-to.
Related
I have an API for storing data and I'm consuming it via my mvc application.
The authentication process, storing the data and everything else is working properly.
The thing is that I am trying to handle some errors or exceptions that may occur in the future.
One of them is handling if my application fails to connect to to my web api and throwing an error or leading the user somewhere else.
My web application controller which consumes the api:
HttpClient client = new HttpClient();
client.BaseAddress = new Uri("https://localhost:44304");
HttpContent content = new FormUrlEncodedContent(
new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("username", lgv.username),
new KeyValuePair<string,string>("password", lgv.password),
new KeyValuePair<string,string>("grant_type","password"),
});
content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
content.Headers.ContentType.CharSet = "UTF-8";
/*this is the line where all the parameters get passed and validates everything*/
var resultDisplay = await client.PostAsync("token", content);
/* **************************************************************************** */
if (resultDisplay.IsSuccessStatusCode)
{
var loginData = await resultDisplay.Content.ReadAsAsync<LoginResponseViewModel>();
Session["access_token"] = loginData.Access_Token;
}
So what I have tried is:
HttpException hc = new HttpException();
switch(hc){
case 200: return view("success"); /* since successful code is 200*/
break;
case 400: return view("error");
break;
}
But it doesn't seem to work because this code only checks the state of the application and not of the api...
So my question is, how can I handle it if my api is not running?
So instead of making the switch on the application make it over resultDisplay.StatusCode instead, this way you are checking the API state using the response status code.
Like this:
switch(resultDisplay.StatusCode){
case 200: return view("success"); /* since successful code is 200*/
break;
case 400: return view("error");
break;
}
I'm currently trying to get push notifications working for my mobile app using Azure Notification Hubs. Android is working fine and the initial iOS set up in AppDelegate works ok with a sample tag.
public override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken)
{
if (deviceToken == null)
{
return;
}
SBNotificationHub hub = new SBNotificationHub(CommonConstants.LISTEN_CONNECTION_STRING, CommonConstants.NOTIFICATION_HUB_NAME);
// update registration with Azure Notification Hub
hub.UnregisterAll(deviceToken, async (error) =>
{
if (error != null)
{
System.Diagnostics.Debug.WriteLine($"Unable to call unregister {error}");
return;
}
string[] tags = new[] { "iostestpush" };
NSSet userTags = new NSSet(tags);
hub.RegisterNative(deviceToken, userTags, (error) =>
{
if (error != null)
{
System.Diagnostics.Debug.WriteLine($"Unable to call register {error}");
return;
}
});
var templateExpiration = DateTime.Now.AddDays(120).ToString(System.Globalization.CultureInfo.CreateSpecificCulture("en-US"));
hub.RegisterTemplate(deviceToken, "defaultTemplate", CommonConstants.APN_TEMPLATE_BODY, templateExpiration, userTags, (errorCallback) =>
{
if (errorCallback != null)
{
System.Diagnostics.Debug.WriteLine($"RegisterTemplateAsync error: {errorCallback}");
}
});
});
}
The issue I'm having is I need to register the UserId after a successful login. So I set up a service with the above code, saved the token to the device as string so it can be retrieved in the service and turned back into an NSData token
NSData deviceToken = new NSData(token, NSDataBase64DecodingOptions.None);
After a successful login I send the token string and the tag array to my service.
string[] userTag = new[] { loginResponse.UserId.ToString() };
await this._azureReg.SendRegistrationToServer(deviceToken, userTag);
Which, other than turning the token back into NSData and the user tag into an NSSet, is the same as above other than the name change. But Azure is claiming there is no registration even though my output shows
Registered for push notifications with token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
I thought it was the string conversion back and forth, so tested that in the AppDelegate and it worked fine.
So, I'm at a loss at how to register the UserId after a successful login and why it works in one place but not the other.
I hope that's clear and thanks for any advice in advance.
You probably ran into the same bug as me and several others.
Basically SBNotificationHub method overloads like UnregisterAll and RegisterTemplate with the callback signature do not work when you use them off the main thread, using the libraries to date. I was also using a Service for the same purpose (to handle push across platforms with different tags, especially for user id) but my implementation involved switching off the main thread for this.
The bug we logged and is now being addressed is here: https://github.com/Azure/azure-notificationhubs-ios/issues/95
The solution, for now, is to ditch SBNotificationHub completely. The Xamarin / Azure documentation is out of date, and SBNOtificationHub is legacy code. The recommended library is MSNotificationHub. https://github.com/azure/azure-notificationhubs-xamarin
As workarounds you can use the SBNotificationHub method overloads that do not involve callbacks (they return an error message instead) or the workaround in the 95 issue above.
I am using AppAuth to implement google sign-in. The app could successfully authenticate. But I need an id_token for my server so that I can communicate with the my server from my app. For that I believe I need to include audience:server:client_id:WEB_CLIENT_ID as shown in the following link.
https://developers.google.com/identity/sign-in/android/v1/backend-auth
More information is available here:
https://developers.google.com/identity/protocols/CrossClientAuth
How can I use my web client id from the app to get an id_token so that I can reliably communicate with my server using that token?
The scope audience:server:client_id:WEB_CLIENT_ID is specific to Android. For iOS we need to send audience=WEB_CLIENT_ID as parameter to token endpoint.
It works in my case using the following code.
OIDServiceConfiguration *configuration = [[OIDServiceConfiguration alloc] initWithAuthorizationEndpoint:authorizationEndpoint tokenEndpoint:tokenEndpoint];
// builds authentication request
OIDAuthorizationRequest *authorizationRequest =
[[OIDAuthorizationRequest alloc] initWithConfiguration:configuration
clientId:kClientId
scopes:#[OIDScopeOpenID,
OIDScopeEmail]
redirectURL:[NSURL URLWithString:kRedirectUri]
responseType:OIDResponseTypeCode
additionalParameters:nil];
// performs authentication request
OIDAuthorizationUICoordinatorIOS *coordinator = [[OIDAuthorizationUICoordinatorIOS alloc]
initWithPresentingViewController:self];
id<OIDAuthorizationFlowSession> authFlowSession = [OIDAuthorizationService
presentAuthorizationRequest:authorizationRequest
UICoordinator:coordinator
callback:^(OIDAuthorizationResponse *_Nullable authorizationResponse,
NSError *_Nullable authorizationError) {
// inspects response and processes further if needed (e.g. authorization
// code exchange)
if (authorizationResponse) {
if ([authorizationRequest.responseType
isEqualToString:OIDResponseTypeCode]) {
// if the request is for the code flow (NB. not hybrid), assumes the
// code is intended for this client, and performs the authorization
// code exchange
OIDTokenRequest *tokenExchangeRequest =
[[OIDTokenRequest alloc] initWithConfiguration:authorizationRequest.configuration
grantType:OIDGrantTypeAuthorizationCode
authorizationCode:authorizationResponse.authorizationCode
redirectURL:authorizationRequest.redirectURL
clientID:authorizationRequest.clientID
clientSecret:authorizationRequest.clientSecret
scope:authorizationRequest.scope
refreshToken:nil
codeVerifier:authorizationRequest.codeVerifier
additionalParameters:#{#"audience":kWebClientId}];
//tokenExchangeRequest.scope = kAudienceServerClientId;
[OIDAuthorizationService
performTokenRequest:tokenExchangeRequest
callback:^(OIDTokenResponse *_Nullable tokenResponse,
NSError *_Nullable tokenError) {
OIDAuthState *authState;
if (tokenResponse) {
authState = [[OIDAuthState alloc]
initWithAuthorizationResponse:
authorizationResponse
tokenResponse:tokenResponse];
}
[self onSignInResponse:authState error:tokenError];
}];
} else {
// implicit or hybrid flow (hybrid flow assumes code is not for this
// client)
OIDAuthState *authState = [[OIDAuthState alloc]
initWithAuthorizationResponse:authorizationResponse];
[self onSignInResponse:authState error:authorizationError];
}
} else {
[self onSignInResponse:nil error:authorizationError];
}
}];
MyAppDelegate *appDelegate = [MyAppDelegate sharedInstance];
appDelegate.currentAuthorizationFlow = authFlowSession;
I've scoured the web on how to do basic http auth in a uiwebview. I've tried it all and am getting inconsistent results no matter what I do. Sometimes auth works, sometimes it doesn't. It's driving me up the wall and I'd love some help!
Here's the relevant code. I always get an auth challenge, but the webview doesn't always load (I basically time out waiting for webViewDidFinishLoad - no errors). Any ideas?
/*
Load a webview
*/
- (void)loadWebView
{
NSLog(#"loading webview");
if( _webView == NULL ){
// UIWebView initialization
_webView = [[UIWebView alloc] init];
_webView.hidden = TRUE;
}
// set up webview request
NSURL *url = [NSURL URLWithString:BASE_URL];
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:30];
// load the request
[_webView setDelegate:self];
[_webView loadRequest:request];
// add ourselves as delegate to connection events so we can authenticate
(void)[NSURLConnection connectionWithRequest:request delegate:self];
}
/**
authenticates against HTTP basic authorization
*/
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
//receive a authenticate and challenge with the user credential
NSLog(#"got auth challenge. Authenticating...");
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodHTTPBasic] &&
[challenge previousFailureCount] == 0)
{
NSURLCredential *credentail = [NSURLCredential
credentialWithUser:AUTH_USERNAME
password:AUTH_PASS
persistence:NSURLCredentialPersistenceForSession];
[[challenge sender] useCredential:credentail forAuthenticationChallenge:challenge];
}
else
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Error Message" message:#"Invalid credentails" delegate:self cancelButtonTitle:#"OK" otherButtonTitles:nil];
[alert show];
}
}
/**
store auth credentials
*/
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection;
{
NSLog(#"using credential storage");
return YES;
}
You can pass authentication in url instead of request header
please try instead of BASE_URL:
NSString* urlString = [NSString stringWithFormat:#"%#//%#:%##%#",#"http:",userName,password,myUrl]
I'm trying to access a page that has xml I'm trying to parse. I'm using the following code which works as long as I do not need credentials. How can I add credentials to this code?
NSURL *url = [[NSURL alloc] initWithString:URLPath];
NSXMLParser *xmlParser = [[NSXMLParser alloc] initWithContentsOfURL:url];
[xmlParser setDelegate: self];
success = [xmlParser parse];
Above my "URLPath" is my url string. I've tried using the following url path format but it doesn't work for me. http://username:password#mypage.com/path/to/file.aspx?Function=Update
Thanks!
Use the NSURLConnectionDelegates
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
if ([challenge previousFailureCount] == 0) {
NSLog(#"received authentication challenge");
NSURLCredential *newCredential = [NSURLCredential credentialWithUser:#"USER"
password:#"PASSWORD"
persistence:NSURLCredentialPersistenceForSession];
NSLog(#"credential created");
[[challenge sender] useCredential:newCredential forAuthenticationChallenge:challenge];
NSLog(#"responded to authentication challenge");
}
else {
NSLog(#"previous authentication failure");
}
}
See NSURLConnection and Basic HTTP Authentication in iOS for more information