Efficiently working with On-Behalf Of access tokens in an ASP.NET Core application - asp.net-core

Note: this is a follow-up of Reusing a Polly retrial policy for multiple Refit endpoints without explicitly managing the HttpClient
When making Refit work with Polly and an Azure AD-based authentication (On Behalf Of flow), I realized that acquiring an OBO token can be very slow (>400ms). The code for acquiring an OBO token based on the current logger in the user access token is shown below:
public async Task<string> GetAccessToken(CancellationToken token)
{
var adSettings = _azureAdOptions.Value;
string[] scopes = new string[] { "https://foo.test.com/access_as_user" };
string? httpAccessToken = _httpContextAccessor.HttpContext?.Request?.Headers[HeaderNames.Authorization]
.ToString()
?.Replace("Bearer ", "");
if (httpAccessToken == null)
throw new ArgumentNullException("Failed to generate access token (OBO flow)");
string cacheKey = "OboToken_" + httpAccessToken;
string oboToken = await _cache.GetOrAddAsync(cacheKey, async () =>
{
IConfidentialClientApplication cca = GetConfidentialClientApplication(adSettings);
var assertion = new UserAssertion(httpAccessToken);
var result = await cca.AcquireTokenOnBehalfOf(scopes, assertion).ExecuteAsync(token);
return result.AccessToken;
},
new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(TokenCacheExpirationInMinutes) });
return oboToken;
}
private IConfidentialClientApplication GetConfidentialClientApplication(AzureAdAuthOptions adSettings)
{
var certMetadata = _azureAdOptions.Value.ClientCertificates[0];
string certPath = certMetadata.CertificateDiskPath;
_logger.LogInformation($"GetAccessToken certificate path = {certPath}");
string certPassword = certMetadata.CertificatePassword;
var certificate = new X509Certificate2(certPath, certPassword);
_logger.LogInformation($"GetAccessToken certificate = {certificate}");
var cca = ConfidentialClientApplicationBuilder
.Create(adSettings.ClientId)
.WithTenantId(adSettings.TenantId)
.WithCertificate(certificate)
// .WithClientSecret(adSettings.ClientSecret)
.Build();
return cca;
}
This seems to work fine (not tested in a production environment though). however, I feel that I am reinventing the wheel here as I managing the OBO token caching myself.
Currently, this flow is used by Refit configuration:
private static IServiceCollection ConfigureResilience(this IServiceCollection services)
{
services
.AddRefitClient(typeof(IBarIntegration), (sp) =>
{
var accessTokenHelperService = sp.GetRequiredService<IAccessTokenHelperService>();
return new RefitSettings
{
AuthorizationHeaderValueGetter = () => accessTokenHelperService.GetAccessToken(default)
};
})
.ConfigureHttpClient((sp, client) =>
{
var BarSettings = sp.GetRequiredService<IOptions<BarApiSettings>>();
string baseUrl = BarSettings.Value.BaseUrl;
client.BaseAddress = new Uri(baseUrl);
})
.AddPolicyHandler(Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.OrResult(x => x.StatusCode is >= HttpStatusCode.InternalServerError or HttpStatusCode.RequestTimeout)
.WaitAndRetryAsync(Backoff.DecorrelatedJitterBackoffV2(TimeSpan.FromSeconds(1), RetryPolicyMaxCount)));
return services;
}
Are there any caveats with the current implementation? I am only interested in possible security, performance or "reinventing-the-wheel" issues.

Related

ServiceStack 6 CredentialsAuthProvider does not return BearerToken

Our shop has been using ServiceStack libraries for several years now without many issues.
Recently, after upgrading from the 5.12 version to the 6.0 version, we are seeing a credential-based authentication request returning no BearerToken.
I have read the release notes section on JWT changes, here, and I understand the "breaking" change, as well as here, where it describes how to revert back to returning the bearer token in the response. However, the suggested change is for the JwtAuthProvider, not the CredentialsAuthProvider. The CredentialsAuthProvider does not have a UseTokenCookie property.
Is there a way to revert back to returning the bearer token in the authentication response, when the auth provider is a CredentialsAuthProvider?
For what it's worth, here is the relevant server code:
public class CustomCredentialsAuthProvider : CredentialsAuthProvider
{
private readonly IDbContext _dbContext;
private const string AuthProviderType = "credentials";
public CustomCredentialsAuthProvider(IDbContext dbContext)
{
_dbContext = dbContext;
}
public override async Task<bool> TryAuthenticateAsync(IServiceBase authService,
string userName, string password, CancellationToken token=default)
{
var userAuthentication = _dbContext.GetUserAuthenticationData(userName);
return (userAuthentication != null && HashHelper.VerifyHash(password, userAuthentication.PasswordSalt, userAuthentication.PasswordHash));
}
public override async Task<IHttpResult> OnAuthenticatedAsync(IServiceBase authService,
IAuthSession session, IAuthTokens tokens,
Dictionary<string, string> authInfo, CancellationToken token=default)
{
var customUserSession = (CustomUserSession)session;
var userId = _dbContext.GetUserId(string.Empty, customUserSession.UserAuthName);
if (!userId.HasValue) throw new AuthenticationException("Unable to locate UserId");
var claims = _dbContext.GetClaims(userId.Value);
var postalCode = _dbContext.GetPostalCode(userId.Value);
HydrateCustomUserSession(claims.ToList(), customUserSession, postalCode);
//Call base method to Save Session and fire Auth/Session callbacks:
return result = await base.OnAuthenticatedAsync(authService, customUserSession, tokens, authInfo, token);
}
private void HydrateCustomUserSession(IReadOnlyCollection<Claims> claims, CustomUserSession customUserSession, string postalCode)
{
customUserSession.AuthProvider = AuthProviderType;
customUserSession.CreatedAt = DateTime.UtcNow;
customUserSession.UserId = Convert.ToInt32(claims.First(item => item.ClaimName == "sub").ClaimValue);
customUserSession.UserStatusId = Convert.ToInt32(claims.First(item => item.ClaimName == "user_status_id").ClaimValue);
customUserSession.UserAuthId = claims.First(item => item.ClaimName == "sub").ClaimValue;
customUserSession.FirstName = claims.First(item => item.ClaimName == "given_name").ClaimValue;
customUserSession.LastName = claims.First(item => item.ClaimName == "family_name").ClaimValue;
customUserSession.Email = claims.First(item => item.ClaimName == "email").ClaimValue;
customUserSession.Company = claims.First(item => item.ClaimName == "company_name").ClaimValue;
customUserSession.CompanyId = Convert.ToInt32(claims.First(item => item.ClaimName == "company_id").ClaimValue);
customUserSession.CompanyTypeId = Convert.ToInt32(claims.First(item => item.ClaimName == "company_type_id").ClaimValue);
customUserSession.PostalCode = postalCode;
var productIds = claims.First(item => item.ClaimName == "product_ids").ClaimValue;
if (!string.IsNullOrEmpty(productIds))
{
customUserSession.ProductIds = productIds.Split(",").Select(int.Parse).ToList();
}
else
{
customUserSession.ProductIds = new List<int>();
}
var productFeatureIds = claims.First(item => item.ClaimName == "product_feature_ids").ClaimValue;
if (!string.IsNullOrEmpty(productFeatureIds))
{
customUserSession.ProductFeatureIds = productFeatureIds.Split(",").Select(int.Parse).ToList();
}
else
{
customUserSession.ProductFeatureIds = new List<int>();
}
var userRoles = claims.First(item => item.ClaimName == "user_roles").ClaimValue;
if (!string.IsNullOrEmpty(userRoles))
{
customUserSession.UserRoles = userRoles.Split(",").Select(x => new UserRole(x)).ToList();
}
else
{
customUserSession.UserRoles = new List<UserRole>();
}
}
}
On the client side, this is a batch-driven processor, not a web app. The authentication portion is here:
private static void GetBearerToken()
{
var authRequest = new Authenticate()
{
UserName = ConfigHelper.ServiceUserName,
Password = ConfigHelper.ServicePassword,
provider = "credentials",
RememberMe = true
};
var authResponse = _authServiceClient.Post(authRequest);
// BearerToken was returned until the auth service was updated to using ServiceStack 6.0
// After using 6.0, BearerToken is empty.
_myOtherServiceClient.BearerToken = authResponse.BearerToken;
}
Once populated, the bearer token is used to authenticate calls to other services.
Thanks for any help!
If you're using JWT, you'll have the JwtAuthProvider configured in the AuthFeature plugin which is what needs to be configured with UseTokenCookie=false in order for the JWT Tokens to be populated on the Response DTO (i.e. instead of using Token Cookies), as explained in the linked docs.
new JwtAuthProvider(AppSettings) {
UseTokenCookie = false
}
Although you should consider using the recommended JWT Token cookies as successful authentication will still result in an authenticated _authServiceClient:
var authResponse = _authServiceClient.Post(authRequest);
// manual JWT handling is unnecessary when using Token Cookies
//_myOtherServiceClient.BearerToken = authResponse.BearerToken;
Since the JWT token is returned in a HTTP Cookie, which will save requiring additional effort from manually handling/populating Bearer Tokens, including server auto-refreshing of expired JWT Bearer Tokens in v6.

Using PublicClientApplicationBuilder and AcquireTokenSilent for chaining Web API calls

I'm working on an AD proof of concept using a console application and PublicClientApplicationBuilder to call Web API A and to call Web API B which also calls Web API A. (API A is just the "Weather" example, and API B just wraps API A.)
My call in API B to HttpContext.VerifyUserHasAnyAcceptedScope(ApiAyeScopes.AccessAsUser) keeps throwing:
IDW10203: The 'scope' or 'scp' claim does not contain scopes 'api://A0000000-1111-2222-3333-444444444444/access_as_user' or was not found.`
How can I resolve this and get the call from API B to API A to work?
I have the direct call to Web API A working. Here's how I authenticate:
static Boolean Authenticate()
{
// See the answer to https://social.msdn.microsoft.com/Forums/en-US/d4b2aff3-eeb1-4204-82ed-ca80232c2523/error-aadsts50076-due-to-a-configuration-change-made-by-your-administrator-or-because-you-moved-to?forum=WindowsAzureAD.
__identityApplication =
__identityApplication
?? PublicClientApplicationBuilder
.Create("000000-1111-2222-3333-444444444444")
.WithAuthority("https://login.microsoftonline.com/me.org/v2.0")
.WithRedirectUri("http://localhost:11596")
.Build();
string[] scopes = new string[] { "api://A0000000-1111-2222-3333-444444444444/access_as_user" };
__authenticationResult =
__identityApplication
.AcquireTokenInteractive(scopes)
.WithExtraScopesToConsent(new String[] { "api://B0000000-1111-2222-3333-444444444444/access_as_user" })
.WithUseEmbeddedWebView(false)
.ExecuteAsync()
.Result;
Console.WriteLine("Logged in as {0}.", __authenticationResult.Account.Username);
return null != __authenticationResult;
}
Here's how I call Web API A from the console, which works:
static List<WeatherForecast> GetWeatherForecast()
{
HttpClient httpClient = new HttpClient();
httpClient.Timeout = Timeout.InfiniteTimeSpan;
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(JwtBearerDefaults.AuthenticationScheme, __authenticationResult.AccessToken);
var response = httpClient.GetAsync("https://localhost:1001/weatherforecast").Result;
var jsonString = response.Content.ReadAsStringAsync().Result;
return Newtonsoft.Json.JsonConvert.DeserializeObject<List<WeatherForecast>>(jsonString);
}
Here's how I call Web API B, which partially works:
static List<WeatherForecast> GetAugmentedWeatherForecast()
{
string[] scopes = new string[] { "api://B0000000-1111-2222-3333-444444444444/access_as_user" };
var apiBeeAuthenticationResult =
__identityApplication
.AcquireTokenSilent(scopes, __authenticationResult.Account)
.ExecuteAsync()
.Result;
HttpClient httpClient = new HttpClient();
httpClient.Timeout = Timeout.InfiniteTimeSpan;
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(JwtBearerDefaults.AuthenticationScheme, apiBeeAuthenticationResult.AccessToken);
var response = httpClient.GetAsync("https://localhost:1101/weatherforecast").Result;
var jsonString = response.Content.ReadAsStringAsync().Result;
return Newtonsoft.Json.JsonConvert.DeserializeObject<List<WeatherForecast>>(jsonString);
}
In Web API B, I have the following:
public class ApiAyeScopes
{
public const String WeatherRead = "api://A0000000-1111-2222-3333-444444444444/ReadWeather";
public const String AccessAsUser = "api://A0000000-1111-2222-3333-444444444444/access_as_user";
}
[AuthorizeForScopes(Scopes = new[] { ApiAyeScopes.AccessAsUser })]
[Authorize(Policy = ApiBeeAuthorizationPolicies.AssignmentToReadAugmentedWeatherRequired)]
[HttpGet]
public async Task<IEnumerable<AugmentedWeatherForecast>> Get()
{
var apiAyeScopes = new String[] { ApiAyeScopes.AccessAsUser };
// See https://learn.microsoft.com/en-us/azure/active-directory/develop/scenario-web-api-call-api-acquire-token?tabs=aspnetcore#code-in-the-controller
HttpContext.VerifyUserHasAnyAcceptedScope(apiAyeScopes);
var originalResult = await _apiAyeClient.GetWeatherForecasts();
return originalResult.Select(wf => new AugmentedWeatherForecast(wf));
}
The code to get the access token is:
String accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(new String[] { ApiAyeScopes.WeatherRead });
It looks like you are looking to resolve your code from API B to API A to work and API B and API A. On-Behalf-Of flow (OBO) serves the use case where an application invokes a service/web API, which in turn needs to call another service/web API.
Learn more here:
https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/on-behalf-of
https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow
The OBO flow is represented by the steps that follow, which are illustrated in the diagram below.
More guidance can be found here: https://github.com/Azure-Samples/ms-identity-aspnet-webapi-onbehalfof

AcquireTokenSilentAsync failed_to_acquire_token_silently

I have a .NET MVC Core 3.1 Webapp running on azure. This webapp is with SSO against Azure AD and is consumming powerbi API and graph API in delegated mode.
All was working fine but now I regularly have failed_to_acquire_token_silently Exceptions when AcquireTokenSilentAsync is triggered. This is not 100% of the times and happears to me a bit randomly.
Let me try to extract what I think are the most relevant code parts.
Startup.cs / ConfigureServices:
services.AddAuthentication("Azures").AddPolicyScheme("Azures", "Authorize AzureAd or AzureAdBearer", options =>
{
options.ForwardDefaultSelector = context =>
{
....
};
})
.AddJwtBearer(x =>
{
.....
})
// For browser access
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
Startup.cs / ConfigureTokenHandling:
private void ConfigureTokenHandling(IServiceCollection services)
{
if (Configuration["AuthWithAppSecret:ClientSecret"] != "")
{
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.ResponseType = Configuration["AuthWithAppSecret:ResponseType"];
options.ClientSecret = Configuration["AuthWithAppSecret:ClientSecret"];
options.Events = new OpenIdConnectEvents
{
OnAuthorizationCodeReceived = async ctx =>
{
HttpRequest request = ctx.HttpContext.Request;
//We need to also specify the redirect URL used
string currentUri = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path);
//Credentials for app itself
var credential = new ClientCredential(ctx.Options.ClientId, ctx.Options.ClientSecret);
//Construct token cache
ITokenCacheFactory cacheFactory = ctx.HttpContext.RequestServices.GetRequiredService<ITokenCacheFactory>();
TokenCache cache = cacheFactory.CreateForUser(ctx.Principal);
var authContext = new AuthenticationContext(ctx.Options.Authority, cache);
string resource = Configuration["PowerBI:PowerBiResourceUrl"];
AuthenticationResult result = await authContext.AcquireTokenByAuthorizationCodeAsync(
ctx.ProtocolMessage.Code, new Uri(currentUri), credential, resource);
//Tell the OIDC middleware we got the tokens, it doesn't need to do anything
ctx.HandleCodeRedemption(result.AccessToken, result.IdToken);
}
};
});
}
}
A controller is like this :
public class ProjectsController : BaseController
{
private readonly ITokenCacheFactory _tokenCacheFactory;
public ProjectsController(MyContext context, IConfiguration configuration, ITokenCacheFactory tokenCacheFactory)
{
_context = context;
_tokenCacheFactory = tokenCacheFactory;
_configuration = configuration;
}
Later triggered by the controller:
static public async Task<string> GetAccessTokenAsync2(IConfiguration _configuration, ITokenCacheFactory _tokenCacheFactory, ClaimsPrincipal User, string resURL, Uri redirectURI)
{
string authority = _configuration["AzureAd:Authority"];
string clientId = _configuration["AzureAd:ClientId"];
string clientSecret = _configuration["AuthWithAppSecret:ClientSecret"];
var cache = _tokenCacheFactory.CreateForUser(User);
var authContext = new AuthenticationContext(authority, cache);
var credential = new ClientCredential(clientId, clientSecret);
var userId = User.GetObjectId();
AuthenticationResult result;
try
{
result = await authContext.AcquireTokenSilentAsync(
resURL,
credential,
new UserIdentifier(userId, UserIdentifierType.UniqueId));
}
catch (AdalException ex)
{
mylog.Info("GetAccessTokenAsync - Adal Ex:" + ex.ErrorCode);
if (ex.ErrorCode == "failed_to_acquire_token_silently")
{
// There are no tokens in the cache.
try
{
PlatformParameters param = new PlatformParameters();
result = await authContext.AcquireTokenAsync(resURL, clientId, redirectURI, param, new UserIdentifier(userId, UserIdentifierType.UniqueId));
}
catch (Exception e)
{
mylog.Error("GetAccessTokenAsync - AcquireTokenAsync" + e.ToString());
throw e;
}
}
else
throw ex;
}
return result.AccessToken;
}
AcquireTokenAsync has been added to turn around the failed_to_acquire_token_silently issue (but it is totaly failling).
Do you have any idea why it is failing from time to time ?
Any other idea how to fix it ?
Thanks!!!
Christian
EDIT 07/04:
Here an example:
2021-04-07 15:18:24.674 +00:00 OnAuthorizationCodeReceived is triggered for user fd918ddf-fbb9-40d2-812b-b01876118f42
2021-04-07 15:18:31.675 +00:00 AcquireTokenSilentAsync - trigger exception userId 'fd918ddf-fbb9-40d2-812b-b01876118f42'
The users is authenticated against AD correctly. A code is received and few seconds later there a failed_to_acquire_token_silently exception raised.
The error failed_to_acquire_token_silently occurs when an access token cannot be found in the cache or the access token is expired.
Code sample here:
// STS
string cloud = "https://login.microsoftonline.com";
string tenantId = "331e6716-26e8-4651-b323-2563936b416e";
string authority = $"{cloud}/{tenantId}";
// Application
string clientId = "65b27a1c-693c-44bf-bf92-c49e408ccc70";
Uri redirectUri = new Uri("https://TodoListClient");
// Application ID of the Resource (could also be the Resource URI)
string resource = "eab51d24-076e-44ee-bcf0-c2dce7577a6a";
AuthenticationContext ac = new AuthenticationContext(authority);
AuthenticationResult result=null;
try
{
result = await ac.AcquireTokenSilentAsync(resource, clientId);
}
catch (AdalException adalException)
{
if (adalException.ErrorCode == AdalError.FailedToAcquireTokenSilently
|| adalException.ErrorCode == AdalError.InteractionRequired)
{
result = await ac.AcquireTokenAsync(resource, clientId, redirectUri,
new PlatformParameters(PromptBehavior.Auto));
}
}
Note that, AcquireTokenSilent does not need to be called in the Client credentials flow (when the application acquires token without a
user, but in its own name)
But you use client credentials flow in your code, you could get access token via AcquireTokenAsync.
clientCredential = new ClientCredential(clientId, appKey);
AuthenticationContext authenticationContext =
new AuthenticationContext("https://login.microsoftonline.com/<tenantId>");
AuthenticationResult result =
await authenticationContext.AcquireTokenAsync("https://resourceUrl",
clientCredential);

Azure web api Unauthorized 401

I have some code that used to call Azure Scheduler to get a token, then using that token, make restful calls. Works a treat.
So i decided to adopt the code into a new app but this time call my own web api hosted on azure. The API is registered in Active directory I have created a secret key etc. When i initiliaze my static httpclient it fetches a token succesfully.
But when i make a call to the API using the token for auth, the response is a 401 "unauthorized", below is the code.
public static class SchedulerHttpClient
{
const string SPNPayload = "resource={0}&client_id={1}&grant_type=client_credentials&client_secret={2}";
private static Lazy<Task<HttpClient>> _Client = new Lazy<Task<HttpClient>>(async () =>
{
string baseAddress = ConfigurationManager.AppSettings["BaseAddress"];
var client = new HttpClient();
client.BaseAddress = new Uri(baseAddress);
await MainAsync(client).ConfigureAwait(false);
return client;
});
public static Task<HttpClient> ClientTask => _Client.Value;
private static async Task MainAsync(HttpClient client)
{
string tenantId = ConfigurationManager.AppSettings["AzureTenantId"];
string clientId = ConfigurationManager.AppSettings["AzureClientId"];
string clientSecret = ConfigurationManager.AppSettings["AzureClientSecret"];
string token = await AcquireTokenBySPN(client, tenantId, clientId, clientSecret).ConfigureAwait(false);
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); //TODO ssmith: const or localization
}
private static async Task<string> AcquireTokenBySPN(HttpClient client, string tenantId, string clientId, string clientSecret)
{
var payload = String.Format(SPNPayload,
WebUtility.UrlEncode(ConfigurationManager.AppSettings["ARMResource"]),
WebUtility.UrlEncode(clientId),
WebUtility.UrlEncode(clientSecret));
var body = await HttpPost(client, tenantId, payload).ConfigureAwait(false);
return body.access_token;
}
private static async Task<dynamic> HttpPost(HttpClient client, string tenantId, string payload)
{
var address = String.Format(ConfigurationManager.AppSettings["TokenEndpoint"], tenantId);
var content = new StringContent(payload, Encoding.UTF8, "application/x-www-form-urlencoded");
using (var response = await client.PostAsync(address, content).ConfigureAwait(false))
{
if (!response.IsSuccessStatusCode)
{
Console.WriteLine("Status: {0}", response.StatusCode);
Console.WriteLine("Content: {0}", await response.Content.ReadAsStringAsync().ConfigureAwait(false));
}//TODO: start removing tests
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsAsync<dynamic>().ConfigureAwait(false);
}
}
}
The above code is the class that creates a httpclient and gets its authorization.
public virtual async Task<T> GetAsync(string apiURL)
{
try
{
_client = await SchedulerHttpClient.ClientTask;
var response = await _client.GetAsync(apiURL);
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsAsync<T>().ConfigureAwait(false);
return responseContent;
}
catch (Exception e)
{
return default(T);
}
}
The above code is a quick lift of my old code simply to test if i can get any results. but as stated it returns a 401.
My question is, is my old code to get authorization incorrect?
<add key="ARMResource" value="https://management.core.windows.net/" />
<add key="TokenEndpoint" value="https://login.windows.net/{0}/oauth2/token" />
<add key="BaseAddress" value="https://mysite.azurewebsites.net" />
As suspected, This particular issue was cause by the incorrect "ARMresource" in the case of a web api it required me to change it to the client id.
Source of answer
Seems my issue was the same, however i suspect i may be able to omit the resource entirely from my SPNPayload string.

Basic Authentication Middleware with OWIN and ASP.NET WEB API

I created an ASP.NET WEB API 2.2 project. I used the Windows Identity Foundation based template for individual accounts available in visual studio see it here.
The web client (written in angularJS) uses OAUTH implementation with web browser cookies to store the token and the refresh token. We benefit from the helpful UserManager and RoleManager classes for managing users and their roles.
Everything works fine with OAUTH and the web browser client.
However, for some retro-compatibility concerns with desktop based clients I also need to support Basic authentication. Ideally, I would like the [Authorize], [Authorize(Role = "administrators")] etc. attributes to work with both OAUTH and Basic authentication scheme.
Thus, following the code from LeastPrivilege I created an OWIN BasicAuthenticationMiddleware that inherits from AuthenticationMiddleware.
I came to the following implementation. For the BasicAuthenticationMiddleWare only the Handler has changed compared to the Leastprivilege's code. Actually we use ClaimsIdentity rather than a series of Claim.
class BasicAuthenticationHandler: AuthenticationHandler<BasicAuthenticationOptions>
{
private readonly string _challenge;
public BasicAuthenticationHandler(BasicAuthenticationOptions options)
{
_challenge = "Basic realm=" + options.Realm;
}
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
{
var authzValue = Request.Headers.Get("Authorization");
if (string.IsNullOrEmpty(authzValue) || !authzValue.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
{
return null;
}
var token = authzValue.Substring("Basic ".Length).Trim();
var claimsIdentity = await TryGetPrincipalFromBasicCredentials(token, Options.CredentialValidationFunction);
if (claimsIdentity == null)
{
return null;
}
else
{
Request.User = new ClaimsPrincipal(claimsIdentity);
return new AuthenticationTicket(claimsIdentity, new AuthenticationProperties());
}
}
protected override Task ApplyResponseChallengeAsync()
{
if (Response.StatusCode == 401)
{
var challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode);
if (challenge != null)
{
Response.Headers.AppendValues("WWW-Authenticate", _challenge);
}
}
return Task.FromResult<object>(null);
}
async Task<ClaimsIdentity> TryGetPrincipalFromBasicCredentials(string credentials,
BasicAuthenticationMiddleware.CredentialValidationFunction validate)
{
string pair;
try
{
pair = Encoding.UTF8.GetString(
Convert.FromBase64String(credentials));
}
catch (FormatException)
{
return null;
}
catch (ArgumentException)
{
return null;
}
var ix = pair.IndexOf(':');
if (ix == -1)
{
return null;
}
var username = pair.Substring(0, ix);
var pw = pair.Substring(ix + 1);
return await validate(username, pw);
}
Then in Startup.Auth I declare the following delegate for validating authentication (simply checks if the user exists and if the password is right and generates the associated ClaimsIdentity)
public void ConfigureAuth(IAppBuilder app)
{
app.CreatePerOwinContext(DbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
Func<string, string, Task<ClaimsIdentity>> validationCallback = (string userName, string password) =>
{
using (DbContext dbContext = new DbContext())
using(UserStore<ApplicationUser> userStore = new UserStore<ApplicationUser>(dbContext))
using(ApplicationUserManager userManager = new ApplicationUserManager(userStore))
{
var user = userManager.FindByName(userName);
if (user == null)
{
return null;
}
bool ok = userManager.CheckPassword(user, password);
if (!ok)
{
return null;
}
ClaimsIdentity claimsIdentity = userManager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie);
return Task.FromResult(claimsIdentity);
}
};
var basicAuthOptions = new BasicAuthenticationOptions("KMailWebManager", new BasicAuthenticationMiddleware.CredentialValidationFunction(validationCallback));
app.UseBasicAuthentication(basicAuthOptions);
// Configure the application for OAuth based flow
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
//If the AccessTokenExpireTimeSpan is changed, also change the ExpiresUtc in the RefreshTokenProvider.cs.
AccessTokenExpireTimeSpan = TimeSpan.FromHours(2),
AllowInsecureHttp = true,
RefreshTokenProvider = new RefreshTokenProvider()
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
}
However, even with settings the Request.User in Handler's AuthenticationAsyncCore method the [Authorize] attribute does not work as expected: responding with error 401 unauthorized every time I try to use the Basic Authentication scheme.
Any idea on what is going wrong?
I found out the culprit, in the WebApiConfig.cs file the 'individual user' template inserted the following lines.
//// Web API configuration and services
//// Configure Web API to use only bearer token authentication.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
Thus we also have to register our BasicAuthenticationMiddleware
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
config.Filters.Add(new HostAuthenticationFilter(BasicAuthenticationOptions.BasicAuthenticationType));
where BasicAuthenticationType is the constant string "Basic" that is passed to the base constructor of BasicAuthenticationOptions
public class BasicAuthenticationOptions : AuthenticationOptions
{
public const string BasicAuthenticationType = "Basic";
public BasicAuthenticationMiddleware.CredentialValidationFunction CredentialValidationFunction { get; private set; }
public BasicAuthenticationOptions( BasicAuthenticationMiddleware.CredentialValidationFunction validationFunction)
: base(BasicAuthenticationType)
{
CredentialValidationFunction = validationFunction;
}
}