How to implement OAuth2 for a single tool, without using it as my application's authorization solution - oauth-2.0

I currently have a MVC site, in .NET Core, backed by a public API. My users must log in (there are no [Anonymous] controllers), and authentication is already successfully being done using the DotNetCore.Authentication provider. All that is well and good.
What I'm now trying to do (by user request) is implement functionality for a user to read and view their Outlook 365 calendar on a page within my site. It doesn't seem too hard on the surface... all I have to do is have them authenticate through microsoftonline with my registered app, and then -- once they have given approval -- redirect back to my app to view their calendar events that I am now able to pull (probably using Graph).
In principle that seems really easy and straightforward. My confusion comes from not being able to implement authentication for a single controller, and not for the entire site. All of the OAuth2 (or OpenID, or OWIN, or whatever your flavor) examples I can find online -- of which there are countless dozens -- all want to use the authorization to control the User.Identity for the whole site. I don't want to change my sitewide authentication protocol; I don't want to add anything to Startup.cs; I don't want anything to scope outside of the one single controller.
tldr; Is there a way to just call https://login.microsoftonline.com/common/oauth2/v2.0/authorize (or facebook, or google, or whatever), and get back a code or token that I can use for that user on that area of the site, and not have it take over the authentication that is already in place for the rest of the site?

For anybody else who is looking for this answer, I've figured out (after much trial and error) how to authenticate for a single user just for a short time, without using middleware that authenticates for the entire application.
public async Task<IActionResult> OfficeRedirectMethod()
{
Uri loginRedirectUri = new Uri(Url.Action(nameof(OfficeAuthorize), "MyApp", null, Request.Scheme));
var azureADAuthority = #"https://login.microsoftonline.com/common";
// Generate the parameterized URL for Azure login.
var authContext = GetProviderContext();
Uri authUri = await authContext.GetAuthorizationRequestUrlAsync(_scopes, loginRedirectUri.ToString(), null, null, null, azureADAuthority);
// Redirect the browser to the login page, then come back to the Authorize method below.
return Redirect(authUri.ToString());
}
public async Task<IActionResult> OfficeAuthorize()
{
var code = Request.Query["code"].ToString();
try
{
// Trade the code for a token.
var authContext = GetProviderContext();
var authResult = await authContext.AcquireTokenByAuthorizationCodeAsync(code, _scopes);
// do whatever with the authResult, here
}
catch (Exception ex)
{
System.Diagnostics.Trace.WriteLine(ex.ToString());
}
return View();
}
public ConfidentialClientApplication GetContext()
{
var clientId = "OfficeClientId;
var clientSecret = "OfficeClientSecret";
var loginRedirectUri = new Uri(#"MyRedirectUri");
TokenCache tokenCache = new MSALSessionCache().GetMsalCacheInstance();
return new ConfidentialClientApplication(
clientId,
loginRedirectUri.ToString(),
new ClientCredential(clientSecret),
tokenCache,
null);
}
I don't know if that will ever be helpful to anybody but me; I just know that it's a problem that doesn't seem to be easily solved by a quick search.

Related

MSAL.NET redirect loop when using graphApi in MVC & blazor with multiple instances

I have created a blazor component that aims to simplify managing users and group of an enterprise application in my ASP.NET MVC website. When I run the code locally, everything works just fine. However, when I deploy my code on the dev environment (in AKS) the code only works if I run one replica.
When I use multiple instances and I try to access the page that calls my blazor component, the page ends up in a redirect loop, and finally shows the Microsoft login interface with an error mentioning that the login was not valid.
This is how my code looks like:
# program.cs
var initialScopes = builder.Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
var cacheOptions = builder.Configuration.GetSection("AzureTableStorageCacheOptions").Get<AzureTableStorageCacheOptions>();
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
.AddDistributedTokenCaches();
builder.Services.Configure<MsalDistributedTokenCacheAdapterOptions>(options =>
{
options.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24);
});
builder.Services.AddDistributedAzureTableStorageCache(options =>
{
options.ConnectionString = cacheOptions.ConnectionString;
options.TableName = cacheOptions.TableName;
options.PartitionKey = cacheOptions.PartitionKey;
options.CreateTableIfNotExists = true;
options.ExpiredItemsDeletionInterval = TimeSpan.FromHours(24);
});
builder.Services.AddSession();
...
# The controller that calls the blazor component
[AuthorizeForScopes(Scopes = new[] { "Application.ReadWrite.All", "Directory.Read.All", "Directory.ReadWrite.All" })]
public async Task<IActionResult> UserManagement()
{
string[] scopes = new string[] { "Application.ReadWrite.All", "Directory.Read.All", "Directory.ReadWrite.All" };
try
{
await _tokenAcquisition
.GetAccessTokenForUserAsync(scopes)
.ConfigureAwait(false);
}
catch (Exception ex)
{
_telemetryClient.TrackException(ex);
}
return View();
}
And this is what happens:
If the page loads, I can see this exception in the pod logs:
What am I doing wrong?
The tenant actually needs to provide admin consent to your web API for the scopes you want to use for replicas for the token taken from cache.
Also when AuthorizeForScopes attribute is specified with scopes ,this needs the exact scopes that is required by that api. MsalUiRequiredException gets thrown in case of incorrect scopes for that api and results in a challenge to user.
This error may also occur even when the acquiretokensilent call will not have a valid cookie anymore for authentication in cache .Please check how acquiretokensilent call works from here in msal net acquire token silently | microsoft docs
When valid scopes are given , please make sure the permissions are granted consent by the admin directly from portal or during user login authentication.
Also as a work around try to use use httpContextAccessor to access
token after authentication .
Reference: c# - Error : No account or login hint was passed to the AcquireTokenSilent call - Stack Overflow
So, the culprit was:
#my controller
await _tokenAcquisition
.GetAccessTokenForUserAsync(scopes)
.ConfigureAwait(false);
Which we were using initially to reauthenticate the graph api component when we were using InMemoryCache.
There is no need to get the access token again when using DistributedTokenCache, and actually that was causing the token to get saved / invalidated in an infinite loop.
Also, in my blazor component, I had to do use the consent handler to force a login:
private async Task<ServicePrincipal> GetPrincipal(AzureAdConfiguration addConfiguration)
{
try
{
return await GraphClient.ServicePrincipals[addConfiguration.PrincipalId].Request()
.Select("id,appRoles, appId")
.GetAsync();
}
catch (Exception ex)
{
ConsentHandler.HandleException(ex);
throw;
}
}

Aspnet core cookie [Authorize] not redirecting on ajax calls

In an asp.net core 3.1 web app with cookie-based authorization I have created a custom validator which executes on the cookie authorization's OnValidatePrincipal event. The validator does a few things, one of those is check in the backend if the user has been blocked. If the user has been blocked, The CookieValidatePrincipalContext.RejectPrincipal() method is executed and the user is signed out using the CookieValidatePrincipalContext.HttpContext.SignOutAsyn(...) method, as per the MS docs.
Here is the relevant code for the validator:
public static async Task ValidateAsync(CookieValidatePrincipalContext cookieValidatePrincipalContext)
{
var userPrincipal = cookieValidatePrincipalContext.Principal;
var userService = cookieValidatePrincipalContext.GetUserService();
var databaseUser = await userService.GetUserBySidAsync(userPrincipal.GetSidAsByteArray());
if (IsUserInvalidOrBlocked(databaseUser))
{
await RejectUser(cookieValidatePrincipalContext);
return;
}
else if (IsUserPrincipalOutdated(userPrincipal, databaseUser))
{
var updatedUserPrincipal = await CreateUpdatedUserPrincipal(userPrincipal, userService);
cookieValidatePrincipalContext.ReplacePrincipal(updatedUserPrincipal);
cookieValidatePrincipalContext.ShouldRenew = true;
}
}
private static bool IsUserInvalidOrBlocked(User user)
=> user is null || user.IsBlocked;
private static async Task RejectUser(CookieValidatePrincipalContext context)
{
context.RejectPrincipal();
await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
And here is the setup for the cookie-based authorization:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(co =>
{
co.LoginPath = #$"/{ControllerHelpers.GetControllerName<AuthenticationController>()}/{nameof(AuthenticationController.Login)}";
co.LogoutPath = #$"/{ControllerHelpers.GetControllerName<AuthenticationController>()}/{nameof(AuthenticationController.Logout)}";
co.ExpireTimeSpan = TimeSpan.FromDays(30);
co.Cookie.SameSite = SameSiteMode.Strict;
co.Cookie.Name = "GioBQADashboard";
co.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = UserPrincipalValidator.ValidateAsync
};
co.Validate();
});
This actually gets called and executed as expected and redirects the user to the login page when they navigate to a new page after having been blocked.
Most of the views have ajax calls to api methods that execute on a timer every 10 seconds. For those calls the credentials also get validated and the user gets signed out. However, after the user has been signed out, a popup asking for user credentials appears on the page:
If the user doesn't enter their credentials and navigate to another page, they get taken to the login page as expected.
If they do enter their credentials, they stay logged in, but their identity appears to be their windows identity...
What is going on here? What I would really want to achieve is for users to be taken to the login page for any request made after they have been signed out.
I have obviously misconfigured something, so that the cookie-based authorization doesn't work properly for ajax requests, but I cannot figure out what it is.
Or is it the Authorization attribute which does not work the way I'm expecting it to?
The code lines look good to me.
This login dialog seems to be the default one for Windows Authentication. Usually, it comes from the iisSettings within the launchSettings.json file. Within Visual Studio you'll find find the latter within your Project > Properties > launchSettings.json
There set the windowsAuthentication to false.
{
"iisSettings": {
"windowsAuthentication": false,
}
}

AuthorizationCodeProvider: Create is never called, how do I generate the authorization code?

I'm setting up my own OAuth2 server. So far, I have succesfully implemented GrantResourceOwnerCredentials in my implementation of OAuthAuthorizationServerProvider. Now, because I am developing an app for our business, I want to implement the OAuth2 Authorization Code grant.
I have tried to follow directions here https://learn.microsoft.com/en-us/aspnet/aspnet/overview/owin-and-katana/owin-oauth-20-authorization-server but in my implementation, I have not found how to reach the Create call of the AuthorizationCodeProvider (which I set in OAuthAuthorizationServerOptions).
I have briefly checked whether accessing the TokenEndpointPath with a (wrong) code parameter works, and in the debugger I see that my AuthorizationCodeProvider's Receive call is hit. Of course there is no success because the code I send is 'sometestcode' instead of a real one, but the code is hit so that means I'm on the right path.
Here's what I have so far:
public override Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
if (OAuthRepository.GetClient(context.ClientId) != null)
{
var expectedRootUri = new Uri(context.Request.Uri, "/");
if (context.RedirectUri.StartsWith(expectedRootUri.AbsoluteUri))
{
context.Validated();
return Task.FromResult<object>(null);
}
}
context.Rejected();
return Task.FromResult<object>(null);
}
public override Task AuthorizeEndpoint(OAuthAuthorizeEndpointContext context)
{
// I know this is wrong but it's just a start and not the focus of this SO question.
context.Response.Redirect(context.AuthorizeRequest.RedirectUri);
context.RequestCompleted();
return Task.FromResult<object>(null);
}
public override Task GrantAuthorizationCode(OAuthGrantAuthorizationCodeContext context)
{
// Needs additional checks, not the focus of my question either
var newTicket = new AuthenticationTicket(context.Ticket.Identity, context.Ticket.Properties);
context.Validated(newTicket);
return Task.FromResult<object>(null);
}
Now, when I call my AuthorizeEndpointPath with a redirect_uri, I am sent to that Uri immediately. I know this is wrong: I should be sent to a separate login page. I'll fix my Web API later to redirect to the correct Uri.
The focus of my question is this: I am now in the process of implementing the login page, but I do not know how to get the authorization code from my WebAPI after the user has logged in. (I'm skipping the consent part for now and assume that if the user is logged in they're okay with it, I'll add giving consent later.)
I am basing my flow on the diagram shared here https://docs.apigee.com/api-platform/security/oauth/oauth-v2-policy-authorization-code-grant-type
I am using Thinktecture IdentityModel to create the login page in an MVC Controller. Now I need to retrieve the authorization code from the Web API in my MVC Controller. And after that I can then redirect the user back to the original client (app) that requested the Authorization Code flow.
To obtain the authorization code from my Web API, I see three methods in Thinktecture's OAuth2Client:
CreateAuthorizeUrl
CreateCodeFlowUrl
RequestAuthorizationCodeAsync
Neither seem to do what I want. How do I proceed so that my WebAPI is called to generate the code?
[HttpGet]
[ImportModelStateFromTempData]
public ActionResult Authorize(string clientId, string returnUrl, string responseType)
{
AuthorizeViewModel viewModel = new AuthorizeViewModel();
...
...
...
return View(viewModel);
}
[HttpPost]
[ExportModelStateToTempData]
public async Task<ActionResult> Authorize(AuthorizeViewModel viewModel)
{
// NOTE: This is in MVC and is postback from *.cshtml View.
OAuth2Client.?????? // <=== How to obtain authorization code from WebAPI?
...
return Redirect(returnUrl);
}
I think I have it correctly setup on the Web API side. I just don't know how to hit the Create part of the flow. I hope someone can help me understand what I am not seeing. I have a blind spot somewhere I think...
How do I have OAuth2Client get me the authorization code from my WebAPI?
I am also using Postman to test my Web API. If anyone can help me get the URL in Web API 2.0 that returns an authorization code, I would also accept that as an answer. Then I can write the code in MVC myself.
Edit
Okay, so I think I found a part of my blind spot. Firstly, I marked `AuthorizeEndpoint' as "not the focus of this SO question", but that was a big mistake.
When I adapt the AuthorizeEndpoint like so:
public override Task AuthorizeEndpoint(OAuthAuthorizeEndpointContext context)
{
System.Security.Claims.ClaimsIdentity ci = new System.Security.Claims.ClaimsIdentity("Bearer");
context.OwinContext.Authentication.SignIn(ci);
context.RequestCompleted();
return Task.FromResult<object>(null);
}
And if I adapt my implementation of AuthorizationCodeProvider.Create like so:
public void Create(AuthenticationTokenCreateContext context)
{
context.Ticket.Properties.IssuedUtc = DateTime.UtcNow;
context.Ticket.Properties.ExpiresUtc = DateTime.UtcNow.AddSeconds(60);
// Some random Guid
context.SetToken(Guid.NewGuid().ToString("n"));
}
Any call to /authorize is redirected to redirect_uri with a query parameter code=<THE_RANDOM_GUID>! :D
Obviously, this implementation is not where it should be, so my question is not yet resolved. Remaining issues:
Right now, anybody can request an authorization code, the client_id is ignored. ValidateClientAuthentication is apparently not hit as part of AuthorizeEndpoint. How do I obtain ClientId in AuthorizeEndpoint?
The authorization code is not coupled to a client. Anyone who intercepts the code could use it. How do I obtain the ClientId in AuthorizationCodeProvider.Create so that I can store it with the code?
The authorization code is not coupled to a user at all, it's an empty ClaimsIdentity. How do I put a user-login page in between and in AuthorizeEndpoint obtain the ClaimsIdentity for the logged-in user?
So, after quite some searching online, I got some success by searching github. Apparently, OAuthAuthorizationServerProvider offers AuthorizeEndpoint and that method should be used for both "Hey, you're not authorized, go log in you!" as well as for "Ahh, okay you're cool, here's an authorization code.". I had expected that OAuthAuthorizationServerProvider would have two separate methods for that, but it doesn't. That explains why on github, I find some projects that implement AuthorizeEndpoint in a rather peculiar way. I've adopted this. Here's an example:
public override async Task AuthorizeEndpoint(OAuthAuthorizeEndpointContext context)
{
if (context.Request.User != null && context.Request.User.Identity.IsAuthenticated)
{
var redirectUri = context.Request.Query["redirect_uri"];
var clientId = context.Request.Query["client_id"];
var authorizeCodeContext = new AuthenticationTokenCreateContext(
context.OwinContext,
context.Options.AuthorizationCodeFormat,
new AuthenticationTicket(
(ClaimsIdentity)context.Request.User.Identity,
new AuthenticationProperties(new Dictionary<string, string>
{
{"client_id", clientId},
{"redirect_uri", redirectUri}
})
{
IssuedUtc = DateTimeOffset.UtcNow,
ExpiresUtc = DateTimeOffset.UtcNow.Add(context.Options.AuthorizationCodeExpireTimeSpan)
}));
await context.Options.AuthorizationCodeProvider.CreateAsync(authorizeCodeContext);
context.Response.Redirect(redirectUri + "?code=" + Uri.EscapeDataString(authorizeCodeContext.Token));
}
else
{
context.Response.Redirect("/account/login?returnUrl=" + Uri.EscapeDataString(context.Request.Uri.ToString()));
}
context.RequestCompleted();
}
Source: https://github.com/wj60387/WebApiOAUthBase/blob/master/OwinWebApiBase/WebApiOwinBase/Providers/OAuthServerProvider.cs
As for my remaining three questions:
Right now, anybody can request an authorization code, the client_id is ignored. ValidateClientAuthentication is apparently not hit as part of AuthorizeEndpoint. How do I obtain ClientId in AuthorizeEndpoint?
Answer: You have to implement `ValidateClientAuthentication'.
The authorization code is not coupled to a client. Anyone who intercepts the code could use it. How do I obtain the ClientId in AuthorizationCodeProvider.Create so that I can store it with the code?
Answer: OAuthAuthorizationServerProvider takes care of this. As long as you set "client_id" in the ticket, it will check that the client that requests an access token for the authorization code is the same.
The authorization code is not coupled to a user at all, it's an empty ClaimsIdentity. How do I put a user-login page in between and in AuthorizeEndpoint obtain the ClaimsIdentity for the logged-in user?
Answer: You create a separate login page. What this does is sign the user in. If your WebAPI uses cookie-based authentication, you can just redirect the user to the AuthorizeEndpoint again. If you use access tokens, your login page has to make a request to `AuthorizeEndpoint' with the access token to obtain an authorization code. (Don't give the access token to the third party. Your login page requests the authorization code and sends that back.) In other words, if you use access tokens then there are two clients involved in this flow.

A simple ASP .NET MVC API controller using roles

I wrote a web application using ASP .NET MVC and authorization system by default. I configured IdentityRole and input through external providers. Using the current database I have created my data context. Now I want to write a Xamarin.Android app and connect to my database, I want a simple API. But the feature that you want to access this API was only available to user with a certain role. The API is really very simple and therefore do not want to add to the draft WCF or WebAPI project. How to do it best?
First, you don't need a separate project to use Web Api; you can use both MVC and Web Api in the same project. For one off endpoints for things like in-site AJAX requests, just creating MVC actions that return JSON or XML would be fine, but if you're talking about a true API, even if it's fairly simplistic, I'd say go Web Api.
You'd protect your Web Api actions much the same as you would your MVC actions, using the [Authorize] attribute. If you need to restrict by role, you just pass a role(s) to that. However, the big difference here, especially if you're serving a mobile app, is that you'll need pass the authorization along with the request. That's generally accomplished using the Authorization header along with a bearer token. Basically, you would need to set up an endpoint that signs a user in and returns a token. Then, each subsequent request that needs authorization includes that token in the header.
I want to finish and to fully answer this question and close this topic. I've been searching for how to add the ability for a mobile client to connect to an existing site on ASP.NET MVC. In my search, I came across a great article Justin Hyland on March 2, 2014
In principle, everything in this article is well and clearly written, but I want to make a tiny contribution for clarity.
Under Setup WebAPIConfig stated that the need
added in the following code to the WebApiConfig Register method
But if we consider the case ASP.NET MVC we don't have such file. It's all very simple, you just need such a file to create the folder App_Start. The contents of the file can be left exactly as it is in the article.
To get rid of the bugs which will inevitably appear we need to install two nuget package: Microsoft.AspNet.WebApi and Microsoft.AspNet.WebApi.Owin.
Excellent! Now we can turn to the method to obtain the token and then adding the token to the query we can get the needed data closed by the attribute [Authorize].
A small remark. If You need to access a method which is closed for a specific role that to the Authenticate method from the article should add a few lines of code. Immediately after the line:
identity.AddClaim(new Claim(ClaimTypes.Name, user));
add the line:
identity.AddClaim(new Claim(ClaimTypes.Role, role));
where role you can get the following, for example:
var userIdentity = UserManager.FindAsync(user, password).Result;
var role = RoleManager.FindById(userIdentity.Roles.First().RoleId).Name;
User and password you have to send a request.
I also want to give an example of code which will send request and receive response. To not have to look for and immediately start coding.
async Task<string> GetToken(string userName, string password)
{
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>( "user", userName ),
new KeyValuePair<string, string> ( "password", password )
}
);
using (var client = new HttpClient())
{
HttpResponseMessage response = await client.PostAsync(APP_PATH + "/Authenticate", content);
var result = await response.Content.ReadAsStringAsync();
return result;
}
}
async Task<string> GetUserInfo(string token)
{
using (var client = CreateClient(token))
{
var response = await client.GetAsync(APP_PATH + "/ValidateToken");
return await response.Content.ReadAsStringAsync();
}
}
HttpClient CreateClient(string accessToken = "")
{
var client = new HttpClient();
if (!string.IsNullOrWhiteSpace(accessToken))
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
}
return client;
}
All have only to call the appropriate methods in the correct order. I hope that is useful to someone.
P.S.
If You create a new project in Visual Studio to get this functionality you just need to tick:

Using OAuth2 refresh tokens in an ASPMVC application

Scenario
I am using the OWIN cookie authentication middleware to protected my site as follows
public void ConfigureAuth(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
ExpireTimeSpan = new TimeSpan(0, 20, 0),
SlidingExpiration = true
});
}
On login, I use the resource owner password flow to call my token service and retrieve both an access and refresh token.
I then add the refresh token, access token and the time the access token expires to my claims and then call the following to to persist this information to my authentication cookie.
HttpContext
.GetOwinContext()
.Authentication
.SignIn(claimsIdentityWithTokenAndExpiresAtClaim);
Then before calling any service, I can retrieve the access token from my current claims and associate it with the service call.
Problem
Before calling any service, I should really check if the access token has expired and if so use the refresh token to get a new one. Once I have a new access token, I can call the service, however I then need to persist a new authentication cookie with the new access token, refresh token and expiry time.
Is there any nice way to do this transparently to the caller of the service?
Attempted solutions
1) Check before calling every service
[Authorize]
public async Task<ActionResult> CallService(ClaimsIdentity claimsIdentity)
{
var accessToken = GetAccessToken();
var service = new Service(accessToken).DoSomething();
}
private string GetAccessToken(ClaimsIdentity claimsIdentity) {
if (claimsIdentity.HasAccessTokenExpired())
{
// call sts, get new tokens, create new identity with tokens
var newClaimsIdentity = ...
HttpContext
.GetOwinContext()
.Authentication
.SignIn(newClaimsIdentity);
return newClaimsIdentity;
} else {
return claimsIdentity.AccessToken();
}
}
This would work, but it's not sustainable. Also I could not longer use dependency injection to inject my services as the service needs the access token at call time and not construction time.
2) Use some kind of service factory
Before create the service with its access token, it would perform the refresh if needed. The issue it that I'm not sure how I can get the factory to return both a service and also set the cookie within the implementation in a nice way.
3) Do it in a action filter instead.
The thinking is that the session cookie has a 20 minutes sliding expiry. On ever page request, I can check if the access token is more than halfway through it's expiry (ie. if the access token has an expiry of an hour, check to see if it has less than 30 minutes to expiry). If so, perform the refresh. The services can rely on the access token not being expired. Lets say you hit the page just before the 30 minutes expiry and stayed on the page for 30 minutes, the assumption is the session timeout (20 minutes idle) will kick in before you call the service and you wil be logged off.
4) Do nothing and catch the exception from calling a service with an expired token
I couldn't figure out a nice way to get a new token and retry the service call again without having to worry about side effects etc. Plus it would be nicer to check for expiration first, rather than wait for the time it takes the service to fail.
Neither of these solutions are particularly elegant. How are others handling this?
Update:
I spent some time looking in to various options on how to implement this efficiently at the server side with your current setup.
There are multiple ways (like Custom-Middleware, AuthenticationFilter, AuthorizationFilter or ActionFilter) to achieve this on the server side. But, looking at these options I would lean towards AuthroziationFilter. The reason are:
AuthroziationFilters gets executed after AuthenticationFilters. So, it is early in the pipe line that you can make a decision of whether to get a new token or not based on expiry time. Also, we can be sure that the user is authenticated.
The scenario we are dealing with is about access_token which is related to authorization than the authentication.
With filters we have the advantage of selectively using it with actions that are explicitly decorated with that filter unlike the custom middleware which gets executed with every request. This is useful as there will be cases where you do not want to get a refreshed token (since the current one is still valid as we are getting new token well before the expiration) when you are not calling any service.
Actionfilters are called little late in the pipeline also we do not have a case for after executing method in an action filter.
Here is a question from Stackoverflow that has some nice details on how to implement an AuthorizationFilter with dependency injection.
Coming to attaching the Authorization header to the service:
This happens inside your action method. By this time you are sure that the token is valid. So I would create an abstract base class that instantiates a HttpClient class and sets the authorization header. The service class implements that base class and uses the HttpClient to call the web service. This approach is clean as consumers of your setup do not have to know how and when you are getting and attaching the token to the outgoing request for web service. Also, you are getting and attaching the refreshed access_token only when you are calling the web service.
Here is some sample code (please note that I haven't fully tested this code, this is to give you an idea of how to implement):
public class MyAuthorizeAttribute : FilterAttribute, IAuthorizationFilter
{
private const string AuthTokenKey = "Authorization";
public void OnAuthorization(AuthorizationContext filterContext)
{
var accessToken = string.Empty;
var bearerToken = filterContext.HttpContext.Request.Headers[AuthTokenKey];
if (!string.IsNullOrWhiteSpace(bearerToken) && bearerToken.Trim().Length > 7)
{
accessToken = bearerToken.StartsWith("Bearer ") ? bearerToken.Substring(7) : bearerToken;
}
if (string.IsNullOrWhiteSpace(accessToken))
{
// Handle unauthorized result Unauthorized!
filterContext.Result = new HttpUnauthorizedResult();
}
// call sts, get new token based on the expiration time. The grace time before which you want to
//get new token can be based on your requirement. assign it to accessToken
//Remove the existing token and re-add it
filterContext.HttpContext.Request.Headers.Remove(AuthTokenKey);
filterContext.HttpContext.Request.Headers[AuthTokenKey] = $"Bearer {accessToken}";
}
}
public abstract class ServiceBase
{
protected readonly HttpClient Client;
protected ServiceBase()
{
var accessToken = HttpContext.Current.Request.Headers["Authorization"];
Client = new HttpClient();
Client.DefaultRequestHeaders.Add("Authorization", accessToken);
}
}
public class Service : ServiceBase
{
public async Task<string> TestGet()
{
return await Client.GetStringAsync("www.google.com");
}
}
public class TestController : Controller
{
[Authorize]
public async Task<ActionResult> CallService()
{
var service = new Service();
var testData = await service.TestGet();
return Content(testData);
}
}
Please note that using the Client Credentials flow from OAuth 2.0 spec is the approach we need to take when calling an API. Also, the JavaScript solution feels more elegant for me. But, I am sure you have requirements that might be forcing you to do it the way you want. Please let me know if you have any questions are comments. Thank you.
Adding access token, refresh token and expires at to the claims and passing it to the following service may not be a good solution. Claims are more suited for identifying the user information/ authorization information. Also, the OpenId spec specifies that the access token should be sent as part of the authorization header only. We should deal with the problem of expired/ expiring tokens in a different way.
At the client, you can automate the process of getting a new access token well before its expiration using this great Javascript library oidc-client. Now you send this new and valid access token as part of your headers to the server and the server will pass it to the following APIs. As a precaution, you can use the same library to validate the expiration time of the token before sending it to the server. This is much cleaner and better solution in my opinion. There are options to silently update the token without the user noticing it. The library uses a an iframe under the hood to update the token. Here is a link for a video in which the author of the library Brock Allen explains the same concepts. The implementation of this functionality is very straightforward. Examples of how the library can be used is here. The JS call we are interested in would look like:
var settings = {
authority: 'http://localhost:5000/oidc',
client_id: 'js.tokenmanager',
redirect_uri: 'http://localhost:5000/user-manager-sample.html',
post_logout_redirect_uri: 'http://localhost:5000/user-manager-sample.html',
response_type: 'id_token token',
scope: 'openid email roles',
popup_redirect_uri:'http://localhost:5000/user-manager-sample-popup.html',
silent_redirect_uri:'http://localhost:5000/user-manager-sample-silent.html',
automaticSilentRenew:true,
filterProtocolClaims: true,
loadUserInfo: true
};
var mgr = new Oidc.UserManager(settings);
function iframeSignin() {
mgr.signinSilent({data:'some data'}).then(function(user) {
log("signed in", user);
}).catch(function(err) {
log(err);
});
}
The mgr is an instance of
FYI, we can achieve similar functionality at the server by building a custom middleware and using it as part of the request flow in a MessageHandler. Please let me know if you have any questions.
Thanks,
Soma.

Resources