This is more of a design/approach question...
I think I'm missing something here. We're building an Asp.Net MVC 5 web application and securing it with Azure AD using the following scenario:
https://azure.microsoft.com/en-us/documentation/articles/active-directory-authentication-scenarios/#web-browser-to-web-application
https://github.com/Azure-Samples/active-directory-dotnet-webapp-openidconnect
The token/cookie is an absolute expiry and expires after one hour. So what does that do for the user experience? Every hour they have to log back in no matter what? In our testing, when the user expires, the browser is redirected back to AD and the user prompted for credentials. This, of course, breaks any AJAX calls we have loading partial views and none of our DevExpress controls are stable as a result.
Based on the response to this SO post: MVC AD Azure Refresh Token via ADAL JavaScript Ajax and KnockoutJs
...what I'm seeing is expected? It seems to me like not a very viable solution for a cloud hosted line-of-business application where users are logged in and working all day.
Am I missing something? Or is this just not an ideal scenario for business apps?
We faced a similar set of problems, as well as the same thoughts about how you could use Azure AD with ASP.NET MVC in web apps with such a low session timeout (60 minutes).
The solution we came up with, that seems to be working (albeit with limited testing), is to have an iFrame on the page that we refresh every 5 minutes.
<iframe sandbox="allow-same-origin allow-scripts allow-popups allow-forms" id="refreshAuthenticationIframe" src="#Url.Action("CheckSessionTimeout", "Home", new { area = "" })" style="display:none;"></iframe>
The "CheckSessionTimeout" page is basically blank.
In a Javascript file referenced by the whole app, we have:
var pageLoadTime = moment();
setInterval(refreshAuthenticationCookies, 1000);
function refreshAuthenticationCookies() {
if (moment().diff(pageLoadTime, "seconds") > 300) {
document.getElementById("refreshAuthenticationIframe").contentDocument.location = "/Home/ForceSessionRefresh";
pageLoadTime = moment();
}
}
(NB: moment is a JS date/time library we use). On the Home controller, we have:
public ActionResult CheckSessionTimeout() => View();
public ActionResult ForceSessionRefresh()
{
HttpContext.GetOwinContext()
.Authentication.Challenge(new AuthenticationProperties { RedirectUri = "/Home/CheckSessiontimeout" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
return null;
}
I am not sure if any of that is the best way/approach. It's just the best we can do to fix up what seems like a set of difficult constraints with Azure AD and ASP.NET MVC apps (that are not SPAs, not using Web API but are using Ajax calls), relative to where we are coming from where none of this matters with on-premises apps doing Kerberos auth (and our user's expectations that session timeout is nothing they want to see or worry about).
There are two ways to handle this (at least this is how we are doing it in our application; it would be interesting to see what AD gurus have to say about this so that we can also fix it if it is not the right way to to do things):
General Approach - Use Refresh Token
When you get an access token from AD, today you get 3 things back - access token, access token expiry and a refresh token. What you do is cache all three of them in your application. Till the time access token is expired, you can simply use that access token. Once the token is expired, you can make use of refresh token to get a new access token. The method in ADAL you want to use for this purpose is AcquireTokenByRefreshToken.
Having said that, you should not take a hard dependency in your application on Refresh Token. Based on the best practices described here, a refresh token can expire or invalidated. Furthermore based on Vittorio's post, a refresh token is not even returned in ADAL version 3. So you may want to consider that.
Other Approach - Acquire Token Silently
Other approach you could take is acquire a new token silently on behalf of the user once the token expires. I believe this requires that a user must sign in manually at least once in your application and follow the OAuth2 flow. The method you want to use is AcquireTokenSilent.
Here's the pseudo code for our approach:
var now = DateTime.UtcNow.Ticks;
if (now <= tokenExpiry && !string.IsNullOrWhiteSpace(accessToken))
return accessToken;
var clientCredential = new ClientCredential(ClientId, ClientSecret);
var authContext = new AuthenticationContext(string.Format("{0}/{1}",
AzureActiveDirectorySignInEndpoint,
azureADTenantId));
AuthenticationResult authResult = null;
if (!string.IsNullOrWhiteSpace(refreshToken))
{
authResult = await authContext.AcquireTokenByRefreshTokenAsync(refreshToken,
clientCredential,
ADEndpoint);
}
else
{
authResult = await authContext.AcquireTokenSilentAsync(Endpoint,
clientCredential,
new UserIdentifier(userId, UserIdentifierType.UniqueId));
}
return authResult.AccessToken;//Also you may want to cache the token again
Related
Just looking into Blazor and my company decided to explore Server Side App (no WASM).
I got the basic project setup and because we want the app to work with out current user db, I modified our User Server (Microservice) to authenticate a user and return a JWT token. In the Login page once we have the token, we authenticate and the cookie is stored.
var auth = await HttpContext.AuthenticateAsync();
auth.Properties.RedirectUri = "/";
var accessToken = new AuthenticationToken()
{
Name = OpenIdConnectParameterNames.AccessToken,
Value = Token
};
AuthenticationToken[] tokens = { accessToken };
auth.Properties.StoreTokens(tokens);
auth.Properties.IsPersistent = true;
await HttpContext.SignInAsync("Cookies", auth.Principal, auth.Properties);
So I want to say at this point everything is working fine.
Now back to the question. I wish to increase security by using Refresh Tokens, and this is where I hit the problem. Every tutorial or guide I google/bing that talks about Refresh tokens goes by the basses that I am using WASM or an API that I call. There are a few articles that say to use Local Storage but then that can bring up a whole new conversation of Local Storage vs Cookies vs Session, and I was hoping to stick with most of the built in stuff.
I understand that Server-Side issues with the HTTP context, hence why the login page is done on a .cshtml page and the fact that the cookies are set as HTTPOnly so JavaScript cannot access them.
So, is there away to do it?
Just to clarify, this is a Server-Side app only, no client apps or API's or anything else and I wish to stick with most of the built in stuff using cookies.
I have an app that is meant to read tenant data from multiple tenants which requires admin consent. So I just need to store one access token and one refresh token from AquireTokenByAuthorizationCodeAsync() for each tenant. So I wondered, if I were to implement a TokenCache extension in such a scenario, would it be necessary to implement TokenCache.AfterAccess and TokenCache.BeforeAccess? Also, when using AquireTokenAsync(), are the cachebits getting overwritten by the new tokens aquired or does it just append to it? If I wanted the old tokens to be overwritten, could I simply use TokenCache.BeforeWrite to clear the old cache?
Basically, this is what I had in mind:
public class ADALTokenCache : TokenCache
{
public Guid TenantId;
public ADALTokenCache(Guid tenantId) : base()
{
TenantId = tenantId;
using (var dbContext = new MyDbContext())
{
byte[] cache = dbContext.TokenCacheSet.FirstOrDefault(c => c.TenantId == TenantId);
if (cache != null)
{
Deserialize(MachineKey.Unprotect(cache, "ADALCache"));
}
}
}
void BeforeWriteNotification(TokenCacheNotificationArgs args)
{
//Could I call Clear() here so that only
//the new token from AquireTokenAsync() is written?
}
}
To answer your questions
The reason why the cache in the samples is as it is, is that several users might sign-in to the application. That will, BTW be the case even if this is the same user in different tenants (the identity might be different). you have examples of implementation in
Custom token cache serialization in Web applications / Web API
indeed when AcquireTokenSilentAsync will refresh the token it will override the previous token in the cache.
However, stepping back
if I understand correctly, in your scenario, your app is not about getting a token to access data for a given user, but rather access tenant data (for the tenant where a user belongs?), and then regularly do some operations.
Wouldn't it be more the case that you'd have a daemon application (but multi-tenant) ? and therefore you might want to use a client credentials flow rather than the authorization code flow.
Since you are using ADAL (V1 endpoint), you could pre-consent the application in the Azure portal? you would not need to sign-in any user? The page on client credential flows has links to ADAL samples for daemon apps.
you might also want to have a look at active-directory-dotnet-daemon-v2 which seems very close to your scenario (but for the Azure AD V2 endpoint). It's easy transposable to the Azure AD V1 endpoint, though, or you could still use the sample as is, but limit the accepted authorities to just a set of tenants.
I have a WCF service that needs to ultimately get a token to talk to a web api service hosted in azure. Our on premise active directory is synced with our azure account.
Initially I had a play in a win forms app and got a token successfully using the following:
AuthenticationResult authResult = authContext.AcquireToken(apiResourceId, clientId, redirectUri);
This though popped up a login dialog so not much use for a windows service. I then investigated the use of AcquireTokenSilent(). This however kept throwing an exception telling me to call AcquireToken so back to square 1.
My next port of call was to look at AcquireTokenByAuthorizationCode(). My problem with this though is how to acquire an authorisation code which is the first parameter.
I've tried:
var url = authContext.GetAuthorizationRequestURL(apiResourceId, clientId, redirectUri, UserIdentifier.AnyUser, string.Empty);
HttpClient hc = new HttpClient();
HttpResponseMessage hrm = hc.GetAsync(url).Result;
This though just returns an html page showing i think a microsoft login page. It certainly doesn't contain any code.
Any ideas on what I am doing wrong?
The AcquireTokenSilent works only if you already have tokens in the cache, which is not the case in your scenario. The AcquireTokenByAuthorizationCode is meant to be sued on the server side.
The main ways in which you can get a token without popping out a dialog on a client are shown in https://github.com/Azure-Samples/active-directory-dotnet-native-headless, but there are important limitations. If they work in your scenario fine, otherwise you might consider creating a persistent cache, priming it by doing one interactive authentication, and then keep using the same cache from your service via AcquireTokenSilent. The cached refresh token will last 90 days as long as you use it at least once every 14 days.
Access tokens periodically expire and, when that happens, need to be refreshed. When an access token expires or at any other time, your application may be able to use a refresh token to obtain a new, valid access token. Server-side web applications, installed applications, and devices all obtain refresh tokens during the authorization process.
I take this information from https://developers.google.com/youtube/v3/guides/authentication
How can I be aware that access_token expired ?
One very common approach is to catch HTTP Errors for all API requests you make. If you run into a 401 error it means the access token is not valid any more and you can try to refresh it and repeat the original request. If the token refresh fails it most likely means that the user has revoked access for your app.
In pseudo code:
result = makeApiRequest();
if (result.status == 401) {
refreshToken();
result = makeApiRequest();
}
Some of the Google APIs client libraries (for example the one for Python) will take care of that for you automatically, but unfortunately not all of them.
I've got credentials of an account with access to Google Analytics,
I'm looking to utilise the Analytics Core Reporting API http://code.google.com/apis/analytics/docs/gdata/home.html
I've found examples which use username/password calling setUserCredentials, but have seen comments this is less secure/has a low request limit (And doesn't exist in the lastest client).
Plus I've seem examples which use oauth, but require user interaction and grant access to the users google account.
However I'm looking to run a service which doesn't require any user interaction, and connects to a predefined google account (un-related to the user viewing it).
I can then store the results in a database, and end users can query the results from the database.
I've seen information about using AccessType = Offline when you first login, which then returns an access token and a refreshtoken.
http://code.google.com/apis/accounts/docs/OAuth2WebServer.html#offline
In my example though, the end user will never login to the application.
Could I have a seperate admin application which gets a refresh token, and stores the refresh token in the config/lookup table?
Then the main application can use the refresh token pulling from the config/lookup table, and get an access token to be able to query the Google Analytics account.
I'm looking for a C# example which uses AccessType = Offline, and seperates out the fetching of the refresh token and using the refresh token to get an access token to query the google analytics account.
Create your app https://code.google.com/apis/console/
For you App, turn on access to Google Analytics, and create an OAuth 2.0 client ID for your website.
Browse to:
https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=YOUR_APP_ID.apps.googleusercontent.com&access_type=offline&scope=https://www.googleapis.com/auth/analytics.readonly&redirect_uri=HTTP://YOUR_CALL_BACK_URL
Having changed YOUR_APP_ID, YOUR_CALL_BACK_URL to the relevant values.
Important to include access_type=offline.
Press Grant Access, this will redirect to HTTP://YOUR_CALL_BACK_URL?code=THIS_IS_YOUR_CODE. Copy the code in the URL.
With the code, request the Refresh Token using CMD prompt.
curl -d "code=THIS_IS_YOUR_CODE&client_id=YOUR_APP_ID.apps.googleusercontent.com&client_secret=YOUR_APPS_SECRET_CODE&redirect_uri=HTTP://YOUR_CALL_BACK_URL&grant_type=authorization_code" https://accounts.google.com/o/oauth2/token
Having changed THIS_IS_YOUR_CODE, YOUR_APP_ID, YOUR_APPS_SECRET_CODE, YOUR_CALL_BACK_URL to the relevant values.
Record the refresh_token returned.
Download the latest version of the Core Reporting V3.0 .net libraries
http://code.google.com/p/google-api-dotnet-client/wiki/Downloads
There is a bug in the current version of Google.Apis.Analytics.v3.cs, to fix this copy the code in this file to your local solution (And don’t reference Google.Apis.Analytics.v3.bin)
http://code.google.com/p/google-api-dotnet-client/source/browse/Services/Google.Apis.Analytics.v3.cs?repo=samples&name=20111123-1.1.4344-beta
And change the property Dimensions from a List<system.string> to a string.
Or you'll get an error like me and this guy did http://www.evolutiadesign.co.uk/blog/using-the-google-analytics-api-with-c-shar/
You can then use your Refresh Token, to generate you an Access Token without user interaction, and use the Access Token to run a report against Google Analytics.
using System;
using DotNetOpenAuth.OAuth2;
using Google.Apis.Authentication.OAuth2;
using AnalyticsService = Google.Apis.Analytics.v3.AnalyticsService;
class Program
{
public static void Main()
{
var client = new WebServerClient(GoogleAuthenticationServer.Description, "YOUR_APP_ID.apps.googleusercontent.com", "YOUR_APPS_SECRET_CODE");
var auth = new OAuth2Authenticator<WebServerClient>(client, Authenticate);
var asv = new AnalyticsService(auth);
var request = asv.Report.Get("2012-02-20", "2012-01-01", "ga:visitors", "ga:YOUR_GOOGLE_ANALYTICS_ACCOUNT_ID");
request.Dimensions = "ga:pagePath";
request.Sort = "-ga:visitors";
request.MaxResults = 5;
var report = request.Fetch();
Console.ReadLine();
}
private static IAuthorizationState Authenticate(WebServerClient client)
{
IAuthorizationState state = new AuthorizationState(new string[]{}) { RefreshToken = "REFRESH_TOKEN" };
client.RefreshToken(state);
return state;
}
}
Great Answer Ian and it helped me to get going in the correct Direction more than any other answer I could find online. Something must have changed in the AnalyticsService object because the line:
var request = asv.Report.Get("2012-02-20", "2012-01-01", "ga:visitors", "ga:YOUR_GOOGLE_ANALYTICS_ACCOUNT_ID");
did not work for me and I had to use the following:
var request = asv.Data.Ga.Get("ga:YOUR_GOOGLE_ANALYTICS_ACCOUNT_ID", "2012-01-01", "2012-02-20", "ga:visitors");
Hopefully this will help others like your answer helped me. Thanks!
Ian's answer helped me a lot but I kept getting an error running the curl command. Did some research and found that the steps to get the access code and refresh token can be made easier by going to https://code.google.com/oauthplayground/ and checking your oAuth configuration settings. Top right of the page there is a settings button. selected "Use your own OAuth credentials". You can get your access code and request a refresh token all from here.
Hope this helps.
You can manually get a refresh token from the OAuth Playground.
If you are needing a refresh token for a Service Account as I was, make sure you
Click on the settings on the right.
Check Use your own OAuth credentials
Fill in your Client ID and Secret
Close the settings
Click the Refresh button on step 2
Then save the refresh token for use in your app