I have an webapplication which uses the Microsoft Graph API to get data from Office365 services. For the login i took the code from Microsofts sample project, which uses ADAL.NET Library for authentification.
When i make an http request it checks if the a request was authentificated. The problem is, when a new session was started no authetification request was performed, although the user is logged in. I get the error message Error Failed to acquire token silently as no token was found in the cache. Call method AcquireToken.
After research i found out that i have to call the Method AcquireTokenSilentlyAsync(), which verifies if an acceptable token is in the cache. I have implemented this method but it always throws an exception. After debugging i saw that there is no ID-Token when a logged in user makes an request. How can i get this ID-Token?
public static AuthProvider Instance { get; } = new AuthProvider();
// Get an access token. First tries to get the token from the token cache.
public async Task<string> GetUserAccessTokenAsync()
{
string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
HttpContextBase httpContextBase = HttpContext.Current.GetOwinContext().Environment["System.Web.HttpContextBase"] as HttpContextBase;
SessionTokenCache tokenCache = new SessionTokenCache(signedInUserID, httpContextBase);
var cachedItems = tokenCache.ReadItems(); // see what's in the cache
AuthenticationContext authContext = new AuthenticationContext(SettingsHelper.Authority, tokenCache);
ClientCredential clientCredential = new ClientCredential(SettingsHelper.ClientId, SettingsHelper.ClientSecret);
string userObjectId = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
UserIdentifier userId = new UserIdentifier(userObjectId, UserIdentifierType.UniqueId);
try
{
AuthenticationResult result = await authContext.AcquireTokenSilentAsync(SettingsHelper.GraphResourceId, clientCredential, userId);
return result.AccessToken;
}
// Unable to retrieve the access token silently.
catch (AdalException ex)
{
HttpContext.Current.Request.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties() { RedirectUri = "/" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
throw new Exception("Error" + $" {ex.Message}");
}
}
public class SessionTokenCache : TokenCache
{
private HttpContextBase context;
private static readonly object FileLock = new object();
private readonly string CacheId = string.Empty;
public string UserObjectId = string.Empty;
public SessionTokenCache(string userId, HttpContextBase context)
{
this.context = context;
this.UserObjectId = userId;
this.CacheId = UserObjectId + "_TokenCache";
AfterAccess = AfterAccessNotification;
BeforeAccess = BeforeAccessNotification;
Load();
}
public void Load()
{
lock (FileLock)
{
Deserialize((byte[])context.Session[CacheId]);
}
}
public void Persist()
{
lock (FileLock)
{
// Reflect changes in the persistent store.
var bytes = Serialize();
var x = System.Text.Encoding.UTF8.GetString(bytes);
context.Session[CacheId] = Serialize();
// After the write operation takes place, restore the HasStateChanged bit to false.
HasStateChanged = false;
}
}
Please go through the sample which helps in how to use MSAL.NET (Microsoft Authentication Library) to obtain an access token and will fix your issue.
Note:- MSAL is recommended since ADAL is going to be depreciated
Related
I am trying to cache Access Token using MSAL by following the tutorial provided here: https://github.com/Azure-Samples/ms-identity-aspnet-webapp-openidconnect
I am using ASP.NET MVC on .NET 4.7.2.
But I am getting error when calling an Microsoft Graph API by getting the token from cache.
I'm getting the error when my code hits this line:
result = app.AcquireTokenSilent(scopes, account).ExecuteAsync().Result;
Following the steps when I get the issue.
Run the code from Visual Studio.
Code hit OnAuthorizationCodeReceived()
Able to get the data from Microsoft.Graph
Sign-in is successfully.
Close the browser.
Sign back in.
Code doesn't hit OnAuthorizationCodeReceived().
Call the Microsoft.Graph
Error, IAccount is null (no token found in cache). I expected to get the token from cache
Sign-in again.
Code hit the OnAuthorizationCodeReceived().
The code I am using:
Startup.cs:
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification context)
{
IConfidentialClientApplication clientApp = MsalAppBuilder.BuildConfidentialClientApplication();
AuthenticationResult result = await clientApp.AcquireTokenByAuthorizationCode(new[] { "User.Read" }, context.Code)
.ExecuteAsync();
}
Class to store token in cache
public static class MsalAppBuilder
{
public static string GetAccountId(this ClaimsPrincipal claimsPrincipal)
{
string oid = claimsPrincipal.GetObjectId();
string tid = claimsPrincipal.GetTenantId();
return $"{oid}.{tid}";
}
private static IConfidentialClientApplication clientapp;
public static IConfidentialClientApplication BuildConfidentialClientApplication()
{
if (clientapp == null)
{
clientapp = ConfidentialClientApplicationBuilder.Create(Globals.clientId)
.WithClientSecret(Globals.clientSecret)
.WithRedirectUri(Globals.redirectUri)
.WithAuthority(new Uri(Globals.authority))
.Build();
// In-memory distributed token cache
clientapp.AddDistributedTokenCache(services =>
{
services.AddDistributedMemoryCache();
services.Configure<MsalDistributedTokenCacheAdapterOptions>(o =>
{
o.Encrypt = true;
});
});
}
return clientapp;
}
}
public static string GetData()
{
IConfidentialClientApplication app = MsalAppBuilder.BuildConfidentialClientApplication();
AuthenticationResult result = null;
var account = app.GetAccountAsync(ClaimsPrincipal.Current.GetAccountId()).Result;
string[] scopes = { "User.Read" };
try
{
// try to get an already cached token
result = app.AcquireTokenSilent(scopes, account).ExecuteAsync().Result;// ConfigureAwait(false);
//some functionality here
}
catch (Exception ex)//MsalUiRequiredException
{
return "error";
}
}
You should clear the token cache because there may have been a cache not Encrypt before
Or you adjust this code
services.Configure<MsalDistributedTokenCacheAdapterOptions>(o =>
{
o.Encrypt = false;
});
In my development environment, I have a user that I just received an OAuth Token for the following scopes.
https://www.googleapis.com/auth/calendar
https://www.googleapis.com/auth/calendar.events
https://www.googleapis.com/auth/calendar.readonly
Everything looks fine and I store the token for the user. I then request to list the calendars for the user and I get the invalid_grant with bad request. I try the same request with another user's token (also in my development environment) and it works correctly.
I originally had only the first scope setup, which is write level access. That is what all existing tokens were created with. During my testing, I added the other scopes.
I have tried updating the NuGet packages for Google APIs in my project.
This is my class that is making the calls.
public class GoogleCalendarAdapter : ICalendarAdapter {
#region attributes
private readonly ISiteAuthTokenQueryRepository _tokenRepo;
private readonly GoogleCalendarSettings _settings;
private const string APPNAME = "REDACTED";
private const string ACL_OWNER = "owner";
private const string ACL_WRITER = "writer";
#endregion
#region ctor
public GoogleCalendarAdapter(ISiteAuthTokenQueryRepository tokenRepo,
GoogleCalendarSettings settings) {
_tokenRepo = tokenRepo;
_settings = settings;
}
#endregion
#region methods
private GoogleAuthorizationCodeFlow BuildAuthorizationCodeFlow() {
return new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer() {
ClientSecrets = BuildClientSecrets(),
Scopes = BuildScopeList()
});
}
private CalendarService BuildCalendarService(SiteAuthToken token) {
return new CalendarService(new BaseClientService.Initializer() {
ApplicationName = APPNAME,
HttpClientInitializer = BuildUserCredential(token)
});
}
private ClientSecrets BuildClientSecrets() {
return new ClientSecrets() {
ClientId = _settings.ClientId,
ClientSecret = _settings.ClientSecret
};
}
private string[] BuildScopeList() {
return new [] { CalendarService.Scope.Calendar };
}
private UserCredential BuildUserCredential(SiteAuthToken token) {
TokenResponse responseToken = new TokenResponse() {
AccessToken = token.AccessToken,
RefreshToken = token.RefreshToken
};
return new UserCredential(BuildAuthorizationCodeFlow(), APPNAME, responseToken);
}
public async Task<List<Cal>> GetAllWritableCalendars(Guid siteGuid) {
SiteAuthToken token = await GetToken(siteGuid);
CalendarService svc = BuildCalendarService(token);
IList<CalendarListEntry> calendars = svc.CalendarList
.List()
.Execute()
.Items;
return calendars.Where(c => c.AccessRole.Equals(ACL_OWNER, StringComparison.CurrentCultureIgnoreCase) ||
c.AccessRole.Equals(ACL_WRITER, StringComparison.CurrentCultureIgnoreCase))
.Select(c => new Cal() {
Id = c.Id,
Name = c.Summary
})
.OrderBy(o => o.Name)
.ToList();
}
public async Task<Cal> GetCalendar(Guid siteGuid, string calendarId) {
SiteAuthToken token = await GetToken(siteGuid);
CalendarService svc = BuildCalendarService(token);
CalendarListEntry entry = svc.CalendarList
.Get(calendarId)
.Execute();
Cal retVal = new Cal() {
Id = entry.Id,
Name = entry.Summary
};
return retVal;
}
private async Task<SiteAuthToken> GetToken(Guid siteGuid) {
SiteAuthToken retVal = await _tokenRepo.GetSiteAuthToken(siteGuid, Constants.OAUTH_PROVIDER_GOOGLE);
if (retVal == null) {
throw new ApplicationException($"Could not find a SiteAuthToken for specified site (SiteGuid: {siteGuid})");
}
return retVal;
}
#endregion
}
Something that's helped me immensely in situations like the one you describe is to use the Google Developer OAuth Playground. By default, you can obtain the grant (and watch the traffic) using OAuthPlayground itself as the client. But then the trick is to go in the [Settings] gear and check the box for [x] Use your own OAuth Credentials and try and authorize your client. IMO this is a very useful debugging tool and I wanted to make sure you are aware of it.
I am trying to access the Microsoft Graph API to obtain a user’s outlook groups.
Here is the code to retrieve the access token:
public static async Task<string> GetGraphAccessTokenAsync()
{
string AzureAdGraphResourceURL = "https://graph.microsoft.com/";
string signedInUserUniqueName = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
var userObjectId = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
var clientCredential = new ClientCredential(SettingsHelper.ClientId, SettingsHelper.AppKey);
var userIdentifier = new UserIdentifier(userObjectId, UserIdentifierType.UniqueId);
AuthenticationContext authContext = new AuthenticationContext(
SettingsHelper.Authority, new ADALTokenCache(signedInUserUniqueName));
var result = await authContext.AcquireTokenSilentAsync(AzureAdGraphResourceURL, clientCredential, userIdentifier);
return result.AccessToken;
}
The method uses a settings helper as follows:
public class SettingsHelper
{
private static string _clientId = ConfigurationManager.AppSettings["ida:ClientID"];
private static string _appKey = ConfigurationManager.AppSettings["ida:Password"];
private static string _tenantId = ConfigurationManager.AppSettings["ida:TenantID"];
private static string _authorizationUri = "https://login.windows.net";
private static string _authority = "https://login.windows.net/{0}/";
private static string _graphResourceId = "https://graph.windows.net";
public static string ClientId
{
get
{
return _clientId;
}
}
public static string AppKey
{
get
{
return _appKey;
}
}
public static string TenantId
{
get
{
return _tenantId;
}
}
public static string AuthorizationUri
{
get
{
return _authorizationUri;
}
}
public static string Authority
{
get
{
return String.Format(_authority, _tenantId);
}
}
public static string AADGraphResourceId
{
get
{
return _graphResourceId;
}
}
}
This is the error that I get:
Failed to acquire token silently. Call method AcquireToken
Exception Details:
Microsoft.IdentityModel.Clients.ActiveDirectory.AdalSilentTokenAcquisitionException : Failed to acquire token silently. Call method AcquireToken
The error occurs specifically at this line:
var result = await authContext.AcquireTokenSilentAsync(AzureAdGraphResourceURL, clientCredential, userIdentifier);
I have checked to ensure that the UserIdentifier matches the value in the cache, but it stills seems to reject the token. Any ideas of where I might be going wrong?
Firs of all, make sure to use the Microsoft graph endpoint (actually you used the Active directory endpoint)
private static readonly string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
private static readonly string appKey = ConfigurationManager.AppSettings["ida:ClientSecret"];
private static readonly string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
private static readonly string tenantId = ConfigurationManager.AppSettings["ida:TenantId"];
private static readonly string postLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];
private static readonly string graphResourceId = "https://graph.microsoft.com";
private static readonly Uri graphEndpointId = new Uri("https://graph.microsoft.com/v1.0/");
Before making a Silent call, you have to make a classic call by retrieving a code. I assume you're in an MVC application.
Here is my Startup.Auth.cs code :
public void ConfigureAuth(IAppBuilder app)
{
ApplicationDbContext db = new ApplicationDbContext();
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = AuthenticationHelper.ClientId,
Authority = AuthenticationHelper.AadInstance + AuthenticationHelper.TenantId,
PostLogoutRedirectUri = AuthenticationHelper.PostLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications()
{
// If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away.
AuthorizationCodeReceived =async (context) =>
{
var code = context.Code;
string signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
try
{
var result = await AuthenticationHelper.GetAccessTokenByCodeAsync(signedInUserID, code);
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
//throw;
}
}
}
});
}
Here is the code I used in my AuthenticationHelper class :
public async static Task<AuthenticationResult> GetAccessTokenByCodeAsync(string signedInUserID, string code)
{
ClientCredential credential = new ClientCredential(clientId, appKey);
AuthenticationContext authContext = new AuthenticationContext(AadInstance + TenantId, new ADALTokenCache(signedInUserID));
AuthenticationResult result = await authContext.AcquireTokenByAuthorizationCodeAsync(
code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId);
return result;
}
and Then, each time I need to make a request to the graph, here is the code I used to get a token :
public async static Task<string> GetTokenForApplicationAsync()
{
string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
string userObjectID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
// get a token for the Graph without triggering any user interaction (from the cache, via multi-resource refresh token, etc)
ClientCredential clientcred = new ClientCredential(clientId, appKey);
// initialize AuthenticationContext with the token cache of the currently signed in user, as kept in the app's database
AuthenticationContext authenticationContext = new AuthenticationContext(AadInstance + TenantId, new ADALTokenCache(signedInUserID));
AuthenticationResult authenticationResult = await authenticationContext.AcquireTokenSilentAsync(GraphResourceId, clientcred, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));
return authenticationResult.AccessToken;
}
An other thhings to notice : Make sure your tenantId is the guid of your tenant. For some reason, sometimes, if you use your tenant name, adal make a difference and could raise this kind of error.
I am trying to programmatically retrieve the HostedServices from Microsoft.Azure.Management.Compute using C#. This requires ServiceClientCredential and I do not know how to get it.
How can I instantiate this class?
I am able to get them using Microsoft.WindowsAzure.Management.Compute but here it returns only the instances under ResourceManager not the classic instances.
First you need to create Active Directory application. See How to: Use the portal to create an Azure AD application and service principal that can access resources
The sample code below uses the nuget package Microsoft.Azure.Management.Compute 13.0.1-prerelease:
public class CustomLoginCredentials : ServiceClientCredentials
{
private string AuthenticationToken { get; set; }
public override void InitializeServiceClient<T>(ServiceClient<T> client)
{
var authenticationContext = new AuthenticationContext("https://login.windows.net/{tenantID}");
var credential = new ClientCredential(clientId: "xxxxx-xxxx-xx-xxxx-xxx", clientSecret: "{clientSecret}");
var result = authenticationContext.AcquireToken(resource: "https://management.core.windows.net/", clientCredential: credential);
if (result == null) throw new InvalidOperationException("Failed to obtain the JWT token");
AuthenticationToken = result.AccessToken;
}
public override async Task ProcessHttpRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (request == null) throw new ArgumentNullException("request");
if (AuthenticationToken == null) throw new InvalidOperationException("Token Provider Cannot Be Null");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", AuthenticationToken);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
//request.Version = new Version(apiVersion);
await base.ProcessHttpRequestAsync(request, cancellationToken);
}
}
Then you can initialize the client like this:
netClient = new Microsoft.Azure.Management.Compute.ComputeManagementClient(new CustomLoginCredentials());
netClient.SubscriptionId = _subscriptionId;
The way you'd do this now is to use ITokenProvider and Microsoft.Rest.TokenCredentials.
public class CustomTokenProvider : ITokenProvider
{
private readonly CustomConfiguration _config;
public CustomTokenProvider(CustomConfiguration config)
{
_config = config;
}
public async Task<AuthenticationHeaderValue> GetAuthenticationHeaderAsync(CancellationToken cancellationToken)
{
// For app only authentication, we need the specific tenant id in the authority url
var tenantSpecificUrl = $"https://login.microsoftonline.com/{_config.TenantId}/";
// Create a confidential client to authorize the app with the AAD app
IConfidentialClientApplication clientApp = ConfidentialClientApplicationBuilder
.Create(_config.ClientId)
.WithClientSecret(_config.ClientSecret)
.WithAuthority(tenantSpecificUrl)
.Build();
// Make a client call if Access token is not available in cache
var authenticationResult = await clientApp
.AcquireTokenForClient(new List<string> { _config.Scope })
.ExecuteAsync();
return new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken);
}
}
And then in your DI configuration
services.AddTransient<IPowerBIClient, PowerBIClient>((provider) =>
{
var config = provider.GetRequiredService<CustomConfiguration>();
var tokenProvider = provider.GetRequiredService<CustomTokenProvider>();
return new PowerBIClient(new Uri(config.BaseUrl), new TokenCredentials(tokenProvider));
});
My example is used with Power BI but would work with anything that needs access to ServiceClientCredentials.
You can use the Nuget package Microsoft.Identity.Client for IConfidentialClientApplication.
A bit later in the game, but this is how we do this in our project. We use the token credentials that is provided by the .net framework to access a managed identity, or visual studio (code) identity, or interactive. And connect to the azure infrastructure API.
internal class CustomTokenProvider : ServiceClientCredentials
{
private const string BearerTokenType = "Bearer";
private TokenCredential _tokenCredential;
private readonly string[] _scopes;
private readonly IMemoryCache _cache;
public CustomTokenProvider(TokenCredential tokenCredential, string[] scopes, IMemoryCache cache)
{
_tokenCredential = tokenCredential ?? throw new ArgumentNullException(nameof(tokenCredential));
_scopes = scopes ?? throw new ArgumentNullException(nameof(scopes));
_cache = cache;
}
public override async Task ProcessHttpRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
var token = await _cache.GetOrCreateAsync("accessToken-tokenProvider." + string.Join("#", _scopes), async e =>
{
var accessToken = await _tokenCredential.GetTokenAsync(new TokenRequestContext(_scopes), cancellationToken);
e.AbsoluteExpiration = accessToken.ExpiresOn;
return accessToken.Token;
});
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(BearerTokenType, token);
await base.ProcessHttpRequestAsync(request, cancellationToken).ConfigureAwait(false);
}
}
Couple of remarks:
The TokenCredential class does not cache tokens and if you don't do it, it will trigger an error at azure due to excessive requests.
Calling a v1 endpoint with v2 calls requires to be a bit creative in the scopes. So when you need to access the management API, provide the following scope "https://management.core.windows.net/.default" and not the user_impersonate scope as specified. This due to some internal conversion on the different endpoints. And '.default' scope is always available and will give yout the on
As #verbedr answered that you can adapt a TokenCredential from the Azure.Identity client library. #antdev answered that you could implement a Microsoft.Rest.ITokenProvider. Another option is to combine both approaches like so:
using Azure.Core;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Rest
{
/// Allows an Azure.Core.TokenCredential to be the Microsoft.Rest.ITokenProvider.
public class TokenCredentialTokenProvider : Microsoft.Rest.ITokenProvider
{
readonly TokenCredential _tokenCredential;
readonly string[] _scopes;
public TokenCredentialTokenProvider(TokenCredential tokenCredential, string[] scopes)
{
_tokenCredential = tokenCredential;
_scopes = scopes;
}
public async Task<AuthenticationHeaderValue> GetAuthenticationHeaderAsync(CancellationToken cancellationToken)
{
var accessToken = await _tokenCredential.GetTokenAsync(new TokenRequestContext(_scopes), cancellationToken);
return new AuthenticationHeaderValue("Bearer", accessToken.Token);
}
}
}
It does not have the caching. You could create a CachingTokenProvider or similar if you needed it. This can be used like so:
var tokenCredentials = new Azure.Identity.DefaultAzureCredential(new Azure.Identity.DefaultAzureCredentialOptions
{
AuthorityHost = Azure.Identity.AzureAuthorityHosts.AzurePublicCloud
});
var restTokenProvider = new Microsoft.Rest.TokenCredentialTokenProvider(tokenCredentials,
new string[] { "https://management.core.windows.net/.default" }
);
var restTokenCredentials = new Microsoft.Rest.TokenCredentials(restTokenProvider);
using var computeClient = new ComputeManagementClient(restTokenCredentials);
// computeClient.BaseUri = // set if using another cloud
computeClient.SubscriptionId = subscriptionId;
var vms = computeClient.VirtualMachines.ListAll();
Console.WriteLine("# of vms " + vms.Count());
This worked for me. Here were the relevant dependencies in my csproj that I used:
<PackageReference Include="Azure.Identity" Version="1.4.0" />
<PackageReference Include="Microsoft.Rest.ClientRuntime" Version="2.3.23" />
<PackageReference Include="Microsoft.Azure.Management.Compute" Version="46.0.0" />
I'm trying to implement OAuth Bearer Authentication with Owin. When an invalid or expired token is passed, the default implementation is to log this as a warning and just don't set an Identity. I however would like to reject the whole request with an error in this case. But how would I do this?
After digging through the code I found out that in OAuthBearerAuthenticationHandler it will parse the token using a fallback mechanism when the provided AuthenticationTokenProvider did not parse any ticket (like the default implementation). This handler will log a warning when the token could not be parsed to any ticket or when it expired.
But I can't find any place to plug in my own logic to what happens when the token is invalid or expired. I could theoretically check this on my own in the AuthenticationTokenProvider, but then I would have to reimplement the logic (= copy it over) for creating and reading the token. Also this seems just out of place, as this class seems to be only responsible for creating and parsing tokens. I also don't see a way to plug in my own implementation of the OAuthBearerAuthenticationHandler in the OAuthBearerAuthenticationMiddleware.
Apparently my best and cleanest shot would be to reimplement the whole middleware, but this also seems very overkill.
What do I overlook? How would I go on about this the best?
edit:
For clarification. I know by not setting an identity the request will be rejected with 401 Unauthorized later in the Web API. But I personally see this as really bad style, silently swallowing an erroneous access token without any notification. This way you don't get to know that your token is crap, you just get to know you're not authorized.
I had a similar issue, i think the answer is to late but someone will come here with a similar problem:
I used this nuget package for validate authentication, but i think any method can help: https://www.nuget.org/packages/WebApi.AuthenticationFilter. You can read its documentation in this site https://github.com/mbenford/WebApi-AuthenticationFilter
AuthenticationFilter.cs
public class AuthenticationFilter : AuthenticationFilterAttribute{
public override void OnAuthentication(HttpAuthenticationContext context)
{
System.Net.Http.Formatting.MediaTypeFormatter jsonFormatter = new System.Net.Http.Formatting.JsonMediaTypeFormatter();
var ci = context.Principal.Identity as ClaimsIdentity;
//First of all we are going to check that the request has the required Authorization header. If not set the Error
var authHeader = context.Request.Headers.Authorization;
//Change "Bearer" for the needed schema
if (authHeader == null || authHeader.Scheme != "Bearer")
{
context.ErrorResult = context.ErrorResult = new AuthenticationFailureResult("unauthorized", context.Request,
new { Error = new { Code = 401, Message = "Request require authorization" } });
}
//If the token has expired the property "IsAuthenticated" would be False, then set the error
else if (!ci.IsAuthenticated)
{
context.ErrorResult = new AuthenticationFailureResult("unauthorized", context.Request,
new { Error = new { Code = 401, Message = "The Token has expired" } });
}
}}
AuthenticationFailureResult.cs
public class AuthenticationFailureResult : IHttpActionResult{
private object ResponseMessage;
public AuthenticationFailureResult(string reasonPhrase, HttpRequestMessage request, object responseMessage)
{
ReasonPhrase = reasonPhrase;
Request = request;
ResponseMessage = responseMessage;
}
public string ReasonPhrase { get; private set; }
public HttpRequestMessage Request { get; private set; }
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
return Task.FromResult(Execute());
}
private HttpResponseMessage Execute()
{
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
System.Net.Http.Formatting.MediaTypeFormatter jsonFormatter = new System.Net.Http.Formatting.JsonMediaTypeFormatter();
response.Content = new System.Net.Http.ObjectContent<object>(ResponseMessage, jsonFormatter);
response.RequestMessage = Request;
response.ReasonPhrase = ReasonPhrase;
return response;
}}
Response examples:
{"Error":{"Code":401,"Message":"Request require authorization"}}
{"Error":{"Code":401,"Message":"The Token has expired"}}
Fonts and inspiration documentation:
//github.com/mbenford/WebApi-AuthenticationFilter
//www.asp.net/web-api/overview/security/authentication-filters
Yeah, I did not find 'good' solution for this,
I also don't see a way to plug in my own implementation of the
OAuthBearerAuthenticationHandler in the
OAuthBearerAuthenticationMiddleware.
Apparently my best and cleanest shot would be to reimplement the whole
middleware, but this also seems very overkill.
Agreed, but that's what I did (before reading your post). I copy & pasted three owin classes, and made it so that it sets property in Owins context, which can be later checked by other handlers.
public static class OAuthBearerAuthenticationExtensions
{
public static IAppBuilder UseOAuthBearerAuthenticationExtended(this IAppBuilder app, OAuthBearerAuthenticationOptions options)
{
if (app == null)
throw new ArgumentNullException(nameof(app));
app.Use(typeof(OAuthBearerAuthenticationMiddlewareExtended), app, options);
app.UseStageMarker(PipelineStage.Authenticate);
return app;
}
}
internal class OAuthBearerAuthenticationHandlerExtended : AuthenticationHandler<OAuthBearerAuthenticationOptions>
{
private readonly ILogger _logger;
private readonly string _challenge;
public OAuthBearerAuthenticationHandlerExtended(ILogger logger, string challenge)
{
_logger = logger;
_challenge = challenge;
}
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
{
try
{
// Find token in default location
string requestToken = null;
string authorization = Request.Headers.Get("Authorization");
if (!string.IsNullOrEmpty(authorization))
{
if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
requestToken = authorization.Substring("Bearer ".Length).Trim();
}
}
// Give application opportunity to find from a different location, adjust, or reject token
var requestTokenContext = new OAuthRequestTokenContext(Context, requestToken);
await Options.Provider.RequestToken(requestTokenContext);
// If no token found, no further work possible
if (string.IsNullOrEmpty(requestTokenContext.Token))
{
return null;
}
// Call provider to process the token into data
var tokenReceiveContext = new AuthenticationTokenReceiveContext(
Context,
Options.AccessTokenFormat,
requestTokenContext.Token);
await Options.AccessTokenProvider.ReceiveAsync(tokenReceiveContext);
if (tokenReceiveContext.Ticket == null)
{
tokenReceiveContext.DeserializeTicket(tokenReceiveContext.Token);
}
AuthenticationTicket ticket = tokenReceiveContext.Ticket;
if (ticket == null)
{
_logger.WriteWarning("invalid bearer token received");
Context.Set("oauth.token_invalid", true);
return null;
}
// Validate expiration time if present
DateTimeOffset currentUtc = Options.SystemClock.UtcNow;
if (ticket.Properties.ExpiresUtc.HasValue &&
ticket.Properties.ExpiresUtc.Value < currentUtc)
{
_logger.WriteWarning("expired bearer token received");
Context.Set("oauth.token_expired", true);
return null;
}
// Give application final opportunity to override results
var context = new OAuthValidateIdentityContext(Context, Options, ticket);
if (ticket != null &&
ticket.Identity != null &&
ticket.Identity.IsAuthenticated)
{
// bearer token with identity starts validated
context.Validated();
}
if (Options.Provider != null)
{
await Options.Provider.ValidateIdentity(context);
}
if (!context.IsValidated)
{
return null;
}
// resulting identity values go back to caller
return context.Ticket;
}
catch (Exception ex)
{
_logger.WriteError("Authentication failed", ex);
return null;
}
}
protected override Task ApplyResponseChallengeAsync()
{
if (Response.StatusCode != 401)
{
return Task.FromResult<object>(null);
}
AuthenticationResponseChallenge challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode);
if (challenge != null)
{
OAuthChallengeContext challengeContext = new OAuthChallengeContext(Context, _challenge);
Options.Provider.ApplyChallenge(challengeContext);
}
return Task.FromResult<object>(null);
}
}
public class OAuthBearerAuthenticationMiddlewareExtended : AuthenticationMiddleware<OAuthBearerAuthenticationOptions>
{
private readonly ILogger _logger;
private readonly string _challenge;
/// <summary>
/// Bearer authentication component which is added to an OWIN pipeline. This constructor is not
/// called by application code directly, instead it is added by calling the the IAppBuilder UseOAuthBearerAuthentication
/// extension method.
///
/// </summary>
public OAuthBearerAuthenticationMiddlewareExtended(OwinMiddleware next, IAppBuilder app, OAuthBearerAuthenticationOptions options)
: base(next, options)
{
_logger = AppBuilderLoggerExtensions.CreateLogger<OAuthBearerAuthenticationMiddlewareExtended>(app);
_challenge = string.IsNullOrWhiteSpace(Options.Challenge) ? (!string.IsNullOrWhiteSpace(Options.Realm) ? "Bearer realm=\"" + this.Options.Realm + "\"" : "Bearer") : this.Options.Challenge;
if (Options.Provider == null)
Options.Provider = new OAuthBearerAuthenticationProvider();
if (Options.AccessTokenFormat == null)
Options.AccessTokenFormat = new TicketDataFormat(
Microsoft.Owin.Security.DataProtection.AppBuilderExtensions.CreateDataProtector(app, typeof(OAuthBearerAuthenticationMiddleware).Namespace, "Access_Token", "v1"));
if (Options.AccessTokenProvider != null)
return;
Options.AccessTokenProvider = new AuthenticationTokenProvider();
}
/// <summary>
/// Called by the AuthenticationMiddleware base class to create a per-request handler.
///
/// </summary>
///
/// <returns>
/// A new instance of the request handler
/// </returns>
protected override AuthenticationHandler<OAuthBearerAuthenticationOptions> CreateHandler()
{
return new OAuthBearerAuthenticationHandlerExtended(_logger, _challenge);
}
}
Then I wrote my own authorization filter, which will be applied globally:
public class AuthorizeAttributeExtended : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
{
var tokenHasExpired = false;
var owinContext = OwinHttpRequestMessageExtensions.GetOwinContext(actionContext.Request);
if (owinContext != null)
{
tokenHasExpired = owinContext.Environment.ContainsKey("oauth.token_expired");
}
if (tokenHasExpired)
{
actionContext.Response = new AuthenticationFailureMessage("unauthorized", actionContext.Request,
new
{
error = "invalid_token",
error_message = "The Token has expired"
});
}
else
{
actionContext.Response = new AuthenticationFailureMessage("unauthorized", actionContext.Request,
new
{
error = "invalid_request",
error_message = "The Token is invalid"
});
}
}
}
public class AuthenticationFailureMessage : HttpResponseMessage
{
public AuthenticationFailureMessage(string reasonPhrase, HttpRequestMessage request, object responseMessage)
: base(HttpStatusCode.Unauthorized)
{
MediaTypeFormatter jsonFormatter = new JsonMediaTypeFormatter();
Content = new ObjectContent<object>(responseMessage, jsonFormatter);
RequestMessage = request;
ReasonPhrase = reasonPhrase;
}
}
my WebApiConfig:
config.Filters.Add(new AuthorizeAttributeExtended());
How my configureOAuth looks like:
public void ConfigureOAuth(IAppBuilder app)
{
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
OAuthBearerOptions = new OAuthBearerAuthenticationOptions()
{
};
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10),
Provider = new SimpleAuthorizationServerProvider(),
RefreshTokenProvider = new SimpleRefreshTokenProvider(),
AuthenticationMode = AuthenticationMode.Active
};
FacebookAuthOptions = new CustomFacebookAuthenticationOptions();
app.UseFacebookAuthentication(FacebookAuthOptions);
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthenticationExtended(OAuthBearerOptions);
}
I will try & get this to main branch of oAuth middleware, it seems like an obvious use case, unless I am missing something.
I came across this problem recently. We wanted to return a JSON message if the user's access token had expired, allowing the consumer web application to silently refresh the access token and re-issue the API request. We also didn't want to rely on the exceptions thrown for token lifetime validation.
Not wanting to re-implement any middleware, we specified the Provider option inside JwtBearerAuthenticationOptions and added a delegate to handle the OnRequestTokenMethod. The delegate checks to see if it can read the token passed to the middleware and sets a boolean inside the OWIN context if it's expired.
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
TokenValidationParameters = tokenValidationParameters,
Provider = new OAuthBearerAuthenticationProvider
{
OnRequestToken = (ctx) =>
{
if (!string.IsNullOrEmpty(ctx.Token))
{
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
if (handler.CanReadToken(ctx.Token))
{
JwtSecurityToken jwtToken = handler.ReadJwtToken(ctx.Token);
if (jwtToken.IsExpired())
ctx.OwinContext.Set<bool>("expiredToken", true);
}
}
return Task.CompletedTask;
}
}
});
For convenience I added a quick extension method to check if a JWT expired:
public static class JwtSecurityTokenExtensions
{
public static bool IsExpired (this JwtSecurityToken token)
{
if (DateTime.UtcNow > token.ValidTo.ToUniversalTime())
return true;
return false;
}
}
We ended up using a middleware to check on the state of that boolean:
app.Use((context, next) =>
{
bool expiredToken = context.Get<bool>("expiredToken");
if (expiredToken)
{
// do stuff
}
return next.Invoke();
});
app.UseStageMarker(PipelineStage.Authenticate);
Not exactly the most efficient code, since we're parsing the token again after the middleware already did and also introducing a new middleware to act on the result of the check, but it's a fresh perspective nonetheless.
If authentication fails (meaning the token is expired) then that layer doesn't set the user, as you said. It's up the the authorization layer (later on) to reject the call. So for your scenario your Web API would need to deny access to an anonymous caller. Use the [Authorize] authorization filter attribute.