I've spent some time over the last few days trying to implement a feature for my web application. The feature should add new events to a users google calendar while they are offline. I read the Google OAuth2 documentation for web server applications and seem to understand the basics of it. I created a link to authorize the application for offline access:
<a target='_blank' href='https://accounts.google.com/o/oauth2/auth?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Ftasks&response_type=code&client_id=<MY CLIENT ID>&access_type=offline&redirect_uri=http%3A%2F%2Flocalhost:49949%2Foauth2callback.aspx'>Grant Tasks Permission</a>
If the user accepts then I capture the refresh token at the redirect uri like this:
private static OAuth2Authenticator<WebServerClient> _authenticator;
protected void Page_Load(object sender, EventArgs e)
{
if (HttpContext.Current.Request["code"] != null)
{
_authenticator = CreateAuthenticator();
_authenticator.LoadAccessToken();
}
Response.Write("Refresh Token: " + _authenticator.State.RefreshToken);
}
private OAuth2Authenticator<WebServerClient> CreateAuthenticator()
{
var provider = new WebServerClient(GoogleAuthenticationServer.Description);
provider.ClientIdentifier = "<MY CLIENT ID>";
provider.ClientSecret = "<MY CLIENT SECRET>";
return new OAuth2Authenticator<WebServerClient>(provider, GetAuthorization);
}
private IAuthorizationState GetAuthorization(WebServerClient client)
{
return client.ProcessUserAuthorization(new HttpRequestInfo(HttpContext.Current.Request));
}
For testing purposes I have been copying the refresh token to a text file for further use.
My problem is using this refresh token for offine access. I have been using this code to refresh the token:
protected void btnGetTasks_Click(object sender, EventArgs e)
{
if (_service == null)
{
_authenticator = CreateAuthenticator();
_service = new TasksService(new BaseClientService.Initializer() { Authenticator = _authenticator });
}
var cl = _service.Tasklists.List().Fetch();
}
private OAuth2Authenticator<WebServerClient> CreateAuthenticator()
{
// Register the authenticator.
var provider = new WebServerClient(GoogleAuthenticationServer.Description);
provider.ClientIdentifier = "<MY CLIENT ID>";
provider.ClientSecret = "<MY CLIENT SECRET>";
var authenticator = new OAuth2Authenticator<WebServerClient>(provider, GetAuthorization);
return authenticator;
}
private IAuthorizationState GetAuthorization(WebServerClient client)
{
string scope = "https://www.googleapis.com/auth/tasks";
IAuthorizationState state = new AuthorizationState(new[] { scope });
state.RefreshToken = "<REFRESH TOKEN FROM FIRST STEP>";
var result = client.RefreshToken(state);
return client.ProcessUserAuthorization();
}
Everything seems fine at this point. When I step through the code I can see the result from client.RefreshToken(state) is true. The issue is when I call this line of code:
_service.Tasklists.List().Fetch();
It returns a (401) unauthorized error from google. I'm looking into the cause but I am not sure how to proceed and I am running short on time with this feature. Any advice would be greatly appreciated. Thanks!
Seems just the act of putting code on here always helps me figure it out a little sooner :)
It now appears this line is unnecessary:
return client.ProcessUserAuthorization();
removing that from the GetAuthorization method and just returning the state passed to RefreshToken has resolved the unauthorized error. I'll leave the question in case it stumps anyone else.
Related
Let me put the problem with a bit of structure.
Context
We have a web application build with Web Forms and hosted in an Azure Web App that authenticates the users against an Azure Active Directory using the OWIN + OpenId Connect standards.
The authentication process works like a charm and users are able to access the application without any problem.
So, whats the issue?
After struggling for many days with it I'm unable to pass any query string parameter to the application through the authentication process. For example, if I try to access the application for the first time through the URL: https://myapp.azurewebsites.net/Default.aspx?param=value. The reason I need to pass this parameter is that it triggers some specific actions in the main page.
The problem is that after the authentication redirects to the webapp's main page the original query string parameters of the request are gone.
The code
The startup class looks like this:
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = Constants.ADTenant.ClientId,
Authority = Constants.ADTenant.Authority,
PostLogoutRedirectUri = Constants.ADTenant.PostLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = context =>
{
var code = context.Code;
ClientCredential credential = new ClientCredential(Constants.ADTenant.ClientId,
Constants.ADTenant.AppKey);
string userObjectID = context.AuthenticationTicket.Identity.FindFirst(
Constants.ADTenant.ObjectIdClaimType).Value;
AuthenticationContext authContext = new AuthenticationContext(Constants.ADTenant.Authority,
new NaiveSessionCache(userObjectID));
if (HttpContext.Current != null)
{
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential,
Constants.ADTenant.GraphResourceId);
AuthenticationHelper.token = result.AccessToken;
AuthenticationHelper.refreshToken = result.RefreshToken;
}
return Task.FromResult(0);
}
}
});
And it works properly!
What I already tried
I've got access to the original request Url by adding an overwrite of the RedirectToIdentityProvider notification:
RedirectToIdentityProvider = (context) =>
{
// Ensure the URI is picked up dynamically from the request;
string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase + context.Request.Uri.PathAndQuery;
context.ProtocolMessage.RedirectUri = appBaseUrl;
return Task.FromResult(0);
}
With this I tried to force the redirect to the main page including the original query string parameter, but then the redirection after authentication breaks and gets stuck in an infinite loop.
I've also tried with changing the redirect url of the application configuration in Azure AD without luck. Also tried to store the query string parameters somewhere else, but the Session is not accessible that early in the process.
Does anyone know what am I doing wrong? Or I'm just asking for something impossible? Any help would be appreciated.
Thank you very much in advance!
I recently had a need to do the exact same thing. My solution may not be the most sophisticated, but simple isn't always bad either.
I have two Authentication Filters...
The first filter is applied to all controllers that could potentially be hit with query string parameters prior to authorization. It checks if the principal is authenticated. If false it caches the complete url string in a cookie. If true it looks for any cookies present and clears them, just for cleanup.
public class AuthCheckActionFilter : ActionFilterAttribute, IAuthenticationFilter
{
public void OnAuthentication(AuthenticationContext filterContext)
{
if (!filterContext.Principal.Identity.IsAuthenticated)
{
HttpCookie cookie = new HttpCookie("OnAuthenticateAction");
cookie.Value = filterContext.HttpContext.Request.Url.OriginalString;
filterContext.HttpContext.Response.Cookies.Add(cookie);
}
else
{
if (filterContext.HttpContext.Request.Cookies.AllKeys.Contains("OnAuthenticateAction"))
{
HttpCookie cookie = filterContext.HttpContext.Request.Cookies["OnAuthenticateAction"];
cookie.Expires = DateTime.Now.AddDays(-1);
filterContext.HttpContext.Response.Cookies.Add(cookie);
}
}
}
public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext)
{
}
}
The second filter is applied only to the default landing page, or in other words where the identity server is redirecting after login. This second filter looks for a cookie and if it exists it calls response.Redirect on cookie value.
public class AutoRedirectFilter : ActionFilterAttribute, IAuthenticationFilter
{
public void OnAuthentication(AuthenticationContext filterContext)
{
if(filterContext.Principal.Identity.IsAuthenticated)
{
if(filterContext.HttpContext.Request.Cookies.AllKeys.Contains("OnAuthenticateAction"))
{
HttpCookie cookie = filterContext.HttpContext.Request.Cookies["OnAuthenticateAction"];
filterContext.HttpContext.Response.Redirect(cookie.Value);
}
}
}
public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext)
{
}
}
we have written a WinRT App connected to a Sharepoint 2013.
We are able to authenticate and login to the sharepoint, but we have problems with the logout 'process'. Login is implemented as follows:
We are setting up a HttpClient with the corresponding user credentials and domain information. The configuration is wrapped in the HttpClientConfig class an delivered to a the HttpClientService which holds the HttpClient object.
After that we retrieve the formdigestValue from the sharepoint and use the token in the X-RequestDigest Header in every request. If the token times out we retrieve a new one.
Here is some code how we implemented the above mentioned authentication.
public async Task Inialize()
{
var httpConfig = new HttpClientConfig();
httpConfig.Headers.Add("Accept", "application/json;odata=verbose");
httpConfig.Headers.Add("User-Agent", _userAgent);
httpConfig.DefaultTimeout = Statics.DEFAULT_NETWORK_TIMEOUT_SECONDS;
httpConfig.PreAuthenticate = true;
httpConfig.NetworkCredentials = new NetworkCredential(username, password, _domain);
_httpClientService.ResetCookies();
_httpClientService.ConfigureHttpClient(httpConfig);
}
The ConfigureHttpClient method disposes an old HttpClient instance and creates a new HttpClient instance, like this:
public void ConfigureHttpClient(HttpClientConfig config, bool disposeCurrent = true)
{
_config = config;
if (disposeCurrent)
{
DisposeHttpClient();
}
_httpClient = CreateHttpClient(config);
if (disposeCurrent)
{
//make sure remove old httpclient and httpclienthandler instances after they are not hold anywhere else
GC.Collect();
}
_httpClientDisposed = false;
}
public HttpClient CreateHttpClient(HttpClientConfig config)
{
_httpClientHandler = _httpClientFactoryService.CreateHttpClientHandler();
_httpClientHandler.CookieContainer = _cookieContainer;
_httpClientHandler.UseCookies = true;
_httpClientHandler.AllowAutoRedirect = config.AllowAutoRedirect;
_httpClientHandler.PreAuthenticate = config.PreAuthenticate;
if (config.NetworkCredentials != null)
{
_httpClientHandler.Credentials = config.NetworkCredentials;
}
var client = _httpClientFactoryService.CreateHttpClient(_httpClientHandler, true);
client.Timeout = TimeSpan.FromSeconds(config.DefaultTimeout);
if (config.UseGzipCompression)
{
if (_httpClientHandler.SupportsAutomaticDecompression)
{
_httpClientHandler.AutomaticDecompression = DecompressionMethods.GZip;
client.DefaultRequestHeaders.AcceptEncoding.Add(StringWithQualityHeaderValue.Parse("gzip"));
}
}
return client;
}
public void DisposeHttpClient()
{
var client = _httpClient;
_httpClientDisposed = true; //set flag before disposing is done to be able to react correctly!
if (client != null)
{
client.Dispose();
}
var handler = _httpClientHandler;
if (handler != null)
{
handler.Dispose();
}
GC.Collect();
}
public async Task<object> InitNewSharepointSession(bool useCookies = true)
{
var config = _httpClientService.CurrentClientConfig;
config.UseCookies = useCookies;
var res = await getRequestDigestAsync();
if (res.IsSuccess)
{
SharepointContextInformation = res.Response;
if (config.Headers.ContainsKey("X-RequestDigest"))
{
config.Headers.Remove("X-RequestDigest");
}
config.Headers.Add("X-RequestDigest", SharepointContextInformation.FormDigestValue);
return new DataServiceResponse<bool>(true);
}
else
{
return new DataServiceResponse<bool>(res.Error);
}
}
The ResetCookies method only disposes the old cookies list:
public void ResetCookies()
{
_cookieContainer = new CookieContainer();
}
As you can see we used some GC.Collect() calls which shows a bit our helplessness according the logout stuff.
For logout, we just dispose our httpclient.
But for some reason, if we login with another user, we sometimes get the data of the previous user which is a pretty high rated bug for us.
Everything works nice if we restart the app, but if we only dispose the current users httpClient we may run in this failure having access with the wrong credential/user context of the previous user.
Another thing I watched is the behaviour after a password change. The old password remains and is valid until the app has been restarted.
So I would be very thankful for some hints or suggestions of a sharepoint REST specialist on how to solve this issue.
I guess you are creating a Universal app for Windows 10. In that case, there is no other option than restarting the app, see this answer.
HTTP credentials are not the same as cookies, so resetting the cookies will not help.
However, if you are using System.Net.Http.HttpClient in a Windows 8/8.1 project (no Universal project), disposing the HttpClient should work.
Example with Windows 8/8.1 template. Do NOT use with Universal template.
private async void Foo()
{
// Succeeds, correct username and password.
await Foo("foo", "bar");
// Fails, wrong username and passord.
await Foo("fizz", "buzz");
}
private async Task Foo(string user, string password)
{
Uri uri = new Uri("http://heyhttp.org/?basic=1&user=foo&password=bar");
HttpClientHandler handler = new HttpClientHandler();
handler.Credentials = new System.Net.NetworkCredential(user, password);
HttpClient client = new HttpClient(handler);
Debug.WriteLine(await client.GetAsync(uri));
}
I've writing some tests on ServiceStack services that require authentication using a custom CredentialAuthenticationProvider.
If I create a test by not authenticating with servicestack, I get a serialization error instead of a 401 error. Serialization error is because the service is redirecting to the MVC login HTML page.
How can I prevent a redirect to MVC when the call is on the /api/ path for serviceStack services so that the service returns a 401 instead?
[TestMethod]
public void DeleteUser_not_authenticated()
{
var client = new JsonServiceClient(STR_APIURL);
var resp1 = client.Post(new Auth() { UserName = "user", Password = "***" });
Assert.IsTrue(resp1.UserName == "user");
var user = client.Get(new GetSupportUser { Email = "test#gmail.com" });
client.Post(new Auth { provider = "logout" });
try
{
var resp = client.Delete(user);
}
catch (HttpException e)
{
Assert.IsTrue(e.GetHttpCode() == 401);
}
}
Edit
Per Mythz suggestion, I tried this in global.aspx, but this didn't stop the redirect:
protected void Application_EndRequest(object src, EventArgs e)
{
ServiceStack.MiniProfiler.Profiler.Stop();
var ctx = (HttpApplication)src;
if (ctx.Request.Path.Contains("/api/"))
ctx.Response.SuppressFormsAuthenticationRedirect = true;
}
If you're using .NET 4.5, you can disable built-in authentication redirect
by setting HttpResponse.SuppressFormsAuthenticationRedirect=true which you can add in your Global.asax.cs:
protected void Application_EndRequest(Object sender, EventArgs e)
{
var ctx = (HttpApplication)sender;
var suppressForPath = ctx.Request.PathInfo.StartsWith("/api");
ctx.Response.SuppressFormsAuthenticationRedirect = suppressForPath;
}
For earlier versions of ASP.NET see ServiceStacks docs on Form Hijacking Prevention for registering a custom SuppressFormsAuthenticationRedirectModule IHtptModule to prevent this.
I changed #mythz answer to the Begin_Request instead of End_Request and it is now preventing redirect to forms authentication and returning 401 response.
protected void Application_BeginRequest(object src, EventArgs e)
{
var ctx = (HttpApplication) src;
var suppressForPath =ctx.Request.Path.StartsWith("/api");
ctx.Response.SuppressFormsAuthenticationRedirect = suppressForPath;
}
I'm trying to create desktop application which will allow to list files and folders on google drive account. On this momment I'm able to do it but there is a one issue. I have to re-login each time I want to open google drive account from my application. Is it possible to use stored locally AccessToken/Refresh tokens in order to avoid re-authorization each time?
Here method which is used to get authorization.
private IAuthorizationState GetAuthorization(NativeApplicationClient arg)
{
IAuthorizationState state = new AuthorizationState(new[] { "https://www.googleapis.com/auth/drive.file", "https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile" });
// Get the auth URL:
state.Callback = new Uri("urn:ietf:wg:oauth:2.0:oob");
UriBuilder builder = new UriBuilder(arg.RequestUserAuthorization(state));
NameValueCollection queryParameters = HttpUtility.ParseQueryString(builder.Query);
queryParameters.Set("access_type", "offline");
queryParameters.Set("approval_prompt", "force");
queryParameters.Set("user_id", email);
builder.Query = queryParameters.ToString();
//Dialog window wich returns authcode
GoogleWebBrowserAuthenticator a = new GooogleWebBrowserAuthenticator(builder.Uri.ToString());
a.ShowDialog();
//Request authorization from the user (by opening a browser window):
string authCode = a.authCode;
// Retrieve the access token by using the authorization code:
return arg.ProcessUserAuthorization(authCode, state);
}
SOLVED:
In order to invoke methods from Google Drive sdk first you need to instance of service:
var provider = new NativeApplicationClient(GoogleAuthenticationServer.Description, GoogleDriveHelper.CLIENT_ID, GoogleDriveHelper.CLIENT_SECRET);
var auth = new OAuth2Authenticator<NativeApplicationClient>(provider, GetAuthorization);
Service = new DriveService(auth);
Those CLIENT_ID and CLIENT_SECRET you will have after you sign up for application in Google API console.
Then you need to define GetAuthorization routine, which might look as following:
private IAuthorizationState GetAuthorization(NativeApplicationClient arg)
{
IAuthorizationState state = new AuthorizationState(new[] { "https://www.googleapis.com/auth/drive.file", "https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile" });
state.Callback = new Uri("urn:ietf:wg:oauth:2.0:oob");
state.RefreshToken = AccountInfo.RefreshToken;
state.AccessToken = AccountInfo.AccessToken;
arg.RefreshToken(state);
return state;
}
It will works if you already have Refresh and Access tokens (at least Refresh). So you need to authorize for some user account first.
Then you can use that Service instance to invoke sdk methods.
Hope it will help someone.
I am using oauth to get acces to google contacts from a desktop application. I have followed the instruction from google here: http://code.google.com/intl/iw-IL/apis/gdata/docs/auth/oauth.html#Examples but I am having problems
here is the code:
OAuthParameters parameters = new OAuthParameters()
{
ConsumerKey = CONSUMER_KEY,
ConsumerSecret = CONSUMER_SECRET,
Scope = SCOPE,
Callback = "http://localhost:10101/callback.htm.txt",
SignatureMethod = "HMAC-SHA1"
};
OAuthUtil.GetUnauthorizedRequestToken(parameters);
string authorizationUrl = OAuthUtil.CreateUserAuthorizationUrl(parameters);
Console.WriteLine(authorizationUrl);
var win = new GoogleAuthenticationWindow(authorizationUrl,parameters);
win.ShowDialog();
OAuthUtil.GetAccessToken(parameters);
inside the window I have the following:
private void BrowserNavigated(object sender, NavigationEventArgs e)
{
if (e.Uri.ToString().Contains("oauth_verifier="))
{
OAuthUtil.UpdateOAuthParametersFromCallback(e.Uri.ToString(), m_parameters);
Close();
}
}
at the last line (OAuthUtil.GetAccessToken(parameters);) I am getting a 400 bad request error and I have no idea why...
After much playing around... I think this is the easiest way to access google api:
Service service = new ContactsService("My Contacts Application");
service.setUserCredentials("mail#gmail.com", "password");
var token = service.QueryClientLoginToken();
service.SetAuthenticationToken(token);
var query = new ContactsQuery(#"https://www.google.com/m8/feeds/contacts/mail#gmail.com/full?max-results=25000");
var feed = (ContactsFeed)service.Query(query);
Console.WriteLine(feed.Entries.Count);
foreach (ContactEntry entry in feed.Entries)
{
Console.WriteLine(entry.Title.Text);
}
much easier than using oauth...