Using Identity Server 3, ClaimsPrinciple null even after successful bearer token authentication - asp.net

I have a test console app which I'm pointing at a local instance of Identity Server 3 to request an access token. The following code does this and returns my token fine (passing a single scope "scope.test.client").
static TokenResponse GetClientToken(string clientId, string clientSecret, string[] scopes)
{
var uri = new Uri(string.Concat(ID_BASE_URI, ID_URL_TOKEN));
var client = new TokenClient(
uri.AbsoluteUri,
clientId,
clientSecret);
return client.RequestClientCredentialsAsync(string.Join(" ", scopes)).Result;
I then use this token to call an API also running locally. This takes the TokenResponse obtained above and passed it to this method:
static void CallApi(string url, TokenResponse response)
{
try
{
using (var client = new HttpClient())
{
client.SetBearerToken(response.AccessToken);
Console.WriteLine(client.GetStringAsync(url).Result);
}
}
catch (Exception x)
{
Console.WriteLine(string.Format("Exception: {0}", x.Message));
}
}
The API (an ASP.NET WebApi project) uses an Owin Startup class to enforce bearer token authentication for all requests:
appBuilder.Map(baseApiUrl, inner =>
{
inner.UseWebApi(GlobalConfiguration.Configuration);
// Enforce bearer token authentication for all API requests
inner.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://identityserver/core",
ValidationMode = ValidationMode.ValidationEndpoint,
RequiredScopes = new[] { "scope.test.client" }
});
});
It also ensures all API requests are handled by a custom authorize attribute:
GlobalConfiguration.Configuration.Filters.Add(new DefaultApiAuthorizeAttribute());
Debugging this API, the first line in my overridden OnAuthorize method (in DefaultApiAuthorizeAttribute) is this:
var caller = actionContext.RequestContext.Principal as System.Security.Claims.ClaimsPrincipal;
If I break on this line I can see that actionContext.RequestContext.Principal is always null. However, I can see that ((System.Web.Http.Owin.OwinHttpRequestContext)actionContext.RequestContext).Request.Headers contains an Authorization header with the bearer token passed from my console app.
So it would seem that the API project is not authenticating the bearer token. Certainly the Identity Server logs suggest it isn't being hit at all after issuing the initial access token. So I'd appreciate your expert advice about why this might not be happening, or at least some pointers about where to look.
I suspect it might have something to do with SSL. Both sites are hosted locally under self-signed SSL certs, although Identity Server is configured to not require SSL and uses the idsrv3test.pfx development certificate for signing. I do have another test MVC web app which delegates authentication to the same IS3 instance which works fine locally, so I believe my IS3 instance is configured correctly.

You need to call UseIdentityServerBearerTokenAuthentication before you call UseWebApi. When you set up an OWIN Middleware Pipeline, the order is important.
In your case, Web API will be handling your requests before they get sent onto Identity Server (if they get sent on at all).

I imagine a range of possible issues could have the impact I described, but in my case I was able to find the cause by adding a diagnostics log to my consuming API. This led me to discover that the problem was an assembly conflict. The Owin middleware was looking for a Newtonsoft.JSON assembly with version 8.0.0.0 but my consuming API (actually running on top of a CMS intance) was using 7.0.0.0.
For anyone else who wants to find the answer fast, rather than spend hours tweaking configurations, here's the documentation that describes how to add this logging: https://identityserver.github.io/Documentation/docsv2/consuming/diagnostics.html

Related

Using Microsoft Authentication Library with External Providers (ASP.NET 6)

I have an Azure App Service developed in .NET 6. I wish to authenticate users against several providers (Azure AD, Google, etc). Upon following the (I think) accurate tutorial I was able to authenticate against Microsoft accounts using MSAL with the following code:
var builder = WebApplication.CreateBuilder(args);
// Managed identity credential
var credential = new DefaultAzureCredential();
// Add key vault for initial access
builder.Configuration.AddAzureKeyVault(new Uri(builder.Configuration.GetValue<string>("Azure:VaultUri")), credential);
// Integrate key vault into Azure app config
builder.Configuration.AddAzureAppConfiguration(options =>
{
// Load config string from vault
var configConnection = builder.Configuration.GetValue<string>("config");
// Connect to config
options.Connect(configConnection);
options.ConfigureKeyVault(kv => kv.SetCredential(credential));
});
// Add services to the container.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// CUSTOM DI
builder.Services.AddSingleton<IDatabaseRepo, CosmosDatabaseRepo>();
// Build
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
The protected controller method is simply decorated with [Authorize].
That works great for Microsoft accounts. However, every attempt to integrate Google into this flow fails. I've done the following:
Follow various example utilizing the Microsoft.AspNetCore.Authentication.Google package. I added AddGoogle method with the client id and secret set. This results in an invalid token error.
The OAuth process works (using Postman) and provides both an access token and id token. But:
The access token fails with invalid_token
The open id token fails saying invalid_token and invalid signature.
If I remove AddMicrosoftIdentityWebApi and only use the AddGoogle method then I always receive a 500 Internal Server Error indicating an auth scheme was not defined. Doesn't matter where I define it- whether in AddAuthorization or decorated on the controller's Authorize attribute.
Attempted using manual JWT validation. This seems problematic because:
It also results in the 500 error whatever I try.
It's not supposed to be required if I understand the packages correctly.
ALSO:
The Google client id has been added to my App Service as an identity provider with the associated App (client) ID.
There's 38974 tutorials out there and none seem to point in the right direction/don't address this issue. Any and all guidance is appreciated.

Azure AD reply url failing on html handler

Via ASP.NET I have created a startup file that will use Azure AD to log in a user
e.g.
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions()
{
ClientId = "42067b8d-b972-44e9-af86-ef60bc6d6fdb",
Authority = "https://login.windows.net/...com",
RedirectUri = "http://localhost:50560/content/story_html5.html",
PostLogoutRedirectUri = "http://localhost:50560/content/story_html5.html",
Scope = OpenIdConnectScope.OpenIdProfile,
ResponseType = OpenIdConnectResponseType.IdToken
});
}
And as you can see my RedirectUri in hitting a static file html file.
On my app registration in Azure portal my manifest for the replyUrls states
"replyUrls": [
"http://localhost:50560/content/story_html5.html"
],
So everything is working and connecting correctly.
(if I use a aspx for example the redirection would work)
However using the .html file I'm getting the error
HTTP Error 405.0 - Method Not Allowed
The page you are looking for cannot be displayed because an invalid
method (HTTP verb) is being used.
All I believe I need to do is add the html handler to Azure AD, does anyone know how to do this?
Thanks
This has nothing to do with Azure AD, but your configuration. Your end. Your Project. Your IIS config. Because sign-in response is a HTTP POST for security reasons. And static files handler in IIS does not accept anything beside GET for obvious reasons.
More information you will find here and there.
First, why would you want to redirect to a static page?! With the redirection after OIDC login, the IdP (Identity Provider, understand Azure AD in that case) sends valuable information which is needed by the OIDC middleware (understand the .UseOpenIdConnectAuthentication method) to be able to verify the token and initialize user session. By sending the sign-in response back to a static page you accomplish couple of things:
You cut out the OIDC middleware from the authentication - it is no longer able to process the response. Because it will not listen on static file requests. Static files are processed outside your OWIN authentication middleware.
Thus not able to verify authenticity of the user.
Thus not able to create secure cookie.
Thus not able to sign-in the user into your application.
Conclusion
Do not change the reply URL for your ASP.NET middleware, unless you explicitly and knowingly want to override the complete handling of sign-in responses.

ASP.NET MVC 5 OWIN ADFS Token validation error

I am working on an ASP.NET MVC application which uses ADFS authentication and have the following error in our log files in production and I'm trying to figure out what the cause of this issue is, as I believe it is preventing some users from accessing our application.
The error is as follows:
System.IdentityModel.Tokens.SecurityTokenExpiredException: IDX10223: Lifetime validation failed. The token is expired.
ValidTo: '08/13/2018 12:59:35'
Current time: '08/13/2018 13:15:34'.
While I can't be sure, since I don't have timestamps of when the error happened, I believe it is causing the classic ASP.NET Server Error in '/' Application and it's the only error I'm seeing in our logs that would correlate with that page appearing.
As I'm searching Stack Overflow, I see references to JWT authorization which is not what our application is using. Or at least we aren't using anything that explicitly uses JWT for authentication, it may be what's happening under the hood. I also see some posts which state that if the authentication server and application server times are not in sync this error can occur; I am working with my IT team to verify these server's clocks are in sync and will update accordingly.
Our application uses a singular MVC route to serve our Angular application, and only enforces authentication on that landing page; our API controllers do not have specific authorization requirements on them (I know, bad security practice, that's a whole other conversation I'm trying to have with my team's architect).
While I wait for the information on the clocks, are there any other possible options I can investigate?
OWIN Startup code
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
// Workaround for this bug: http://katanaproject.codeplex.com/workitem/197
app.UseKentorOwinCookieSaver();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
CookieSecure = CookieSecureOption.Always,
CookieName = "Adfs Cookie Name",
});
app.UseWsFederationAuthentication(new WsFederationAuthenticationOptions
{
MetadataAddress = WebConfigurationManager.AppSettings["WSFederation:MetadataAddress"],
Wtrealm = WebConfigurationManager.AppSettings["WSFederation:Realm"],
SignOutWreply = WebConfigurationManager.AppSettings["WSFederation:Realm"],
Notifications = new WsFederationAuthenticationNotifications
{
RedirectToIdentityProvider = ctx =>
{
if (IsAjaxRequest(ctx.Request))
{
ctx.HandleResponse();
}
return Task.FromResult(0);
}
}
});
return app;
}
private static bool IsAjaxRequest(IOwinRequest request)
{
var query = request.Query;
if (query != null && query["X-Requested-With"] == "XMLHttpRequest")
{
return true;
}
var headers = request.Headers;
if (headers != null && headers["X-Requested-With"] == "XMLHttpRequest")
{
return true;
}
return false;
}
}
Finally figured out what the issue was! But first, some background. The web app I'm building integrates with a WPF application via a browser control in the application. The browser control is a tab that is not selected on the initial load of the application, but the does at least make a request and get redirected to ADFS for authentication. However, the browser wasn't completing the redirect from ADFS to my app until after the browser tab is activated.
Why does all of this matter? Well the ADFS token was configured with a 1hr lifetime. So what happened was users would open the WPF app, and automatically authenticate with ADFS and get a token generated. However, if they didn't activate the tab within that 1hr lifetime the token would expire before the redirect completed. I think this could also happen if I opened a tab in a browser, signed in to ADFS, and then immediately moved to a different tab before my app would have time to be served. Ultimately, it's a weird edge case for my application, but the root of the problem was a token getting issued but not validated by my app server until after it had already expired.

IdentityServer 3 refresh user with refresh token

We are trying to set up Identity Server 3 in the right way.
We got authentication working fine and we manage to retrieve the refresh token.
The client application is using Angular.
Now when the acces_token expires any calls to the rest api fails (we managed to get it to return 401) but we are wondering how to re-authenticate the user.
In our tests, any api call made from Javascript is failing (401) but as soon as the page is refreshed the whole mechanism is kicking in. We do see that we are redirected to the identity server but it does not show up the login page, we are sent back to the client application with new tokens apparently.
What I would like to do is to refresh the access token without having to force the user to refresh the page.
What I'm not sure though is whose responsibility is it? Is that the client application (website) or the angular application? In other word, should the application handle this transparently for Angular or should angular do something when it receives a 401, in which case, I'm not too sure how the information will flow back to the web app.
Any clue?
Additional Information: We are using OpenId Connect
I got it working!
As I said in the comments I used this article. The writer is referencing a very nice lib that I am using as well.
Facts:
Identity Server 3 is requesting the client secret upon access token refresh
One should not store the refresh_token or the client_secret on the javascript application as they are considered unsafe (see the article)
So I chose to send the refresh_token as en encrypted cookie sith this class (found of ST BTW, just can't find the link anymore, sorry...)
public static class StringEncryptor
{
public static string Encrypt(string plaintextValue)
{
var plaintextBytes = plaintextValue.Select(c => (byte) c).ToArray();
var encryptedBytes = MachineKey.Protect(plaintextBytes);
return Convert.ToBase64String(encryptedBytes);
}
public static string Decrypt(string encryptedValue)
{
try
{
var encryptedBytes = Convert.FromBase64String(encryptedValue);
var decryptedBytes = MachineKey.Unprotect(encryptedBytes);
return new string(decryptedBytes.Select(b => (char)b).ToArray());
}
catch
{
return null;
}
}
}
The javascript application is getting the value from the cookie. It then deletes the cookie to avoid that thing to be sent over and over again, it is pointless.
When the access_token becomes invalid, I send an http request to the application server with the encrypted refresh_token. That is an anonymous call.
The server contacts the identity server and gets a new access_token that is sent back to Javascript. The awesome library queued all other requests so when I'm back with my new token, I can tell it to continue with authService.loginConfirmed();.
The refresh is actually pretty easy as all you have to do is to use the TokenClient from IdentityServer3. Full method code:
[HttpPost]
[AllowAnonymous]
public async Task<JsonResult> RefreshToken(string refreshToken)
{
var tokenClient = new TokenClient(IdentityServerConstants.IdentityServerUrl + "/connect/token", "my-application-id", "my-application-secret");
var response = await tokenClient.RequestRefreshTokenAsync(StringEncryptor.Decrypt(refreshToken));
return Json(new {response.AccessToken});
}
Comments are welcome, this is probably the best way to do that.
For future reference - using refresh tokens in an angular (or other JS) application is not the correct way as a refresh token is too sensitive to store in the browser. You should use silent renew based on the identityserver cookie to get a new access token. Also see the oidc-client-js javascript library, as this can manage silent renew for you.

Cache database results in WebAPI authentication

I am creating a RESTful web service using ASP.NET WebApi. I am requiring that all incoming requests go over SSL because I will be using Basic HTTP Authentication. The client is responsible for including the credentials in the request header and the web service will authenticate the client on each request.
The actual authentication requires making a database call. Is there a way to cache the database results (the username and password) so that I don't have to make a database call for every incoming request that occurs in a short period of time?
So when a request comes in the web service will look for the username/password combo in the cache. If it is found it will process the request. If it isn't found, it will make the database call to authenticate and then add the username/password to the cache.
The easiest cache that I can think of to use would be using System.Web.Caching. Below is an extremely simplified version of adding a value to the current cache and then retrieving it for processing. I would not recommend using this code as is.
// GET api/values/5
[HttpGet]
public string GetValue(int id)
{
HttpContext.Current.Cache.Insert("2", "test1");
var value = Convert.ToString(HttpContext.Current.Cache.Get(id.ToString()));
return !string.IsNullOrEmpty(value) ? value : "nothing found";
}

Resources