.Net SignalR use JWT Bearer Authentication when Cookie Authentication is also configured - c#

I have a ASP.NET 5 WebApp that is part of a bigger system and uses Cookie Authentication for Browser requests.
I want to add the ability to request data and perform specific actions on certain Windows services that are also part of the overall system and are executed on a couple of seperate PCs. I want to use SignalR for this.
Then Windows-Services are running as a dedicated service identity that is part of our ActiveDirectory. Since the services shall not store their user credentials in code or local configuration files, they are requesting an authentication token for the web application from an API that works with Windows Authentication.
Then, when establishing the SignalR connection with the web app, the services will use the token received from the API to authenticate against the web app. This is working in general.
The Authentication configuration of the web app is:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Login";
options.ExpireTimeSpan = TimeSpan.FromMinutes(12);
options.SlidingExpiration = true;
})
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, opt =>
{
// Configuration details excluded
// ...
opt.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
// ...
}
};
According to Microsoft Documentation this should be a vaild authentication configuration.
In services.AddAuthorization(...) method I've added a policy specific for Bearer scheme:
options.AddPolicy("SignalRService", policy =>
{
policy.RequireRole("SignalRService");
policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
});
And then there is a SignalR Hub Method secured with this policy:
[Authorize(Policy = "SignalRService")]
public async Task RegisterService(string clientIdString) { /*...*/ }
And finally the hub connection in the windows service is created as follows:
connection = new HubConnectionBuilder()
.WithUrl(hubAddress, options =>
{
options.AccessTokenProvider = () => Task.FromResult(authToken);
})
.WithAutomaticReconnect()
.Build();
Establishing the connection works:
await connection.StartAsync();
But when I try to call the hub method from the windows service like await connection.InvokeAsync("RegisterService", clientId); I receive a HubException with the message:
Failed to invoke 'RegisterService' because user is unauthorized
I have also created an API Controller on the web app for testing purposes and secured it with the same policy:
[HttpGet]
[Authorize(Policy = "SignalRService")]
public IActionResult Get()
{
return Ok(User.Identity.Name);
}
When I call this API endpoint with the same token i would user for SignalR Hub call, I get the identity set on the token returned as expected. I also verified that the configured OnMessageReceived event handler is executed in this scenario, while it isn't when I use SignalR connection.
When I set JwtBearerDefaults.AuthenticationScheme as the default scheme in Startup.cs instead of CookieAuthenticationDefaults.AuthenticationScheme it works also with the SignalR Hub, but then my standard Cookie based user authenticaton breaks.
I expect that there is some additonal configuration necessary to tell the web app to explicitely use the Bearer scheme when a Hub method is called, but I could not find anything so far.

After desperately trying for another hour, I found out that the specific bearer authentication worked with Cookie authentication as the default, when I put the Authorize(Policy = "SignalRService") directly on the class instead of on the method.
Since my hub should also be accessible for browser connections using cookies, I finally ended up with:
[Authorize(AuthenticationSchemes = "Bearer,Cookies")]
public class SignalRServiceHub : Hub
{
[Authorize(Policy = "SignalRService")]
public async Task RegisterService(string clientIdString)
{
// ...
}
[Authorize(Policy = "Root")]
public async Task RegisterMonitoringClient()
{
// ...
}
I'm not exactly sure why specifying the Schemes on class level is necessary in this case while it isn't for ApiController implementations

Related

Microsoft Identity Web: Change Redirect Uri

I am using .net 5, Identity Web Ui to access Microsoft Graph. Where can I configure my Redirect URI?
I need to specify the full Uri, since the generated one from callbackUri is incorrect due to being behind a Load Balancer with SSL offload.
Here is my current ConfigureServices section
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
I was facing a similar problem with a WebApp exposed only behind a front door, the WebApp had to call a custom downstream WebApi.
My service configuration that worked on my localhost dev machine:
// AzureAdB2C
services
.AddMicrosoftIdentityWebAppAuthentication(
Configuration,
"AzureAdB2C", subscribeToOpenIdConnectMiddlewareDiagnosticsEvents: true)
.EnableTokenAcquisitionToCallDownstreamApi(p =>
{
p.RedirectUri = redUri; // NOT WORKING, WHY?
p.EnablePiiLogging = true;
},
[... an array with my needed scopes]
)
.AddInMemoryTokenCaches();
I tried the AddDownstreamWebApi but did not manage to make it work so I just fetched the needed token with ITokenAcquisition and added it to an HttpClient to make my request.
Then I needed AzureAd/B2C login redirect to the uri with the front door url:
https://example.org/signin-oidc and things broke. I solved it like this:
First of all you have to add this url to your App registration in the azure portal, very important is case sensitive it cares about trailing slashes and I suspect having many urls that point to the very same controller and the order of these have some impact, I just removed everything and kept the bare minimum.
Then in the configure services method:
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.SaveTokens = true; // this saves the token for the downstream api
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = async ctxt =>
{
// Invoked before redirecting to the identity provider to authenticate. This can be used to set ProtocolMessage.State
// that will be persisted through the authentication process. The ProtocolMessage can also be used to add or customize
// parameters sent to the identity provider.
ctxt.ProtocolMessage.RedirectUri = "https://example.org/signin-oidc";
await Task.Yield();
}
};
});
With that the redirect worked, but I entered a loop between the protected page and the AzureB2C login.
After a succesful login and a correct redirect to the signin-oidc controller (created by the Identity.Web package) I was correctly redirected again to the page that started all this authorization thing, but there it did not find the authorization. So I added/modded also this:
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
options.Secure = CookieSecurePolicy.Always;
});
With this the authorization worked, but I was not able to get the token to call the downstream API, before this redirect thing ITokenAcquisition worked, but now when trying to get the token it throws an exception.
So in my controller/service to get the token I modified and used:
var accessToken = await _contextAccessor.HttpContext
.GetTokenAsync(OpenIdConnectDefaults.AuthenticationScheme, "access_token");
So now with the token I add it to my HttpRequestMessage like this:
request.Headers.Add("Authorization", $"Bearer {accessToken}");
I lived on StackOverflow and microsoft docs for 3 days, I am not sure this is all "recommended" but this worked for me.
I had the same problem running an asp.net application under Google Cloud Run, which terminates the TLS connection. I was getting the error:
AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application.
Using fiddler, I examined the request to login.microsoftonline.com and found that the query parameter redirect_uri exactly matched the url I'd configured in the application in Azure except that it started http rather than https.
I initially tried the other answers involving handling the OpenIdConnectEvents event and updating the redirect uri. This fixed the redirect_url parameter in the call to login.microsoftonline.com and it then worked until I added in the graph api. Then I found my site's signin-oidc page would give its own error about the redirect uri not matching. This would then cause it to go into a loop between my site and login.microsoftonline.com repeatedly trying to authenticate until eventually I'd get a login failure.
On further research ASP.net provides middleware to properly handle this scenario. Your SSL load balancer should add the standard header X-Forwarded-Proto with value HTTPS to the request. It should also send the X-Forwarded-For header with the originating IP address which could be useful for debugging, geoip etc.
In your ASP.net application, to configure the middleware:
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
Then enable the middleware:
app.UseForwardedHeaders();
Importantly, you must include this before the calls to app.UseAuthentication/app.UseAuthorization that depends on it.
Source: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-5.0
If your load balancer doesn't add the X-Forwarded-Proto header and can't be configured to do so then the document above outlines other options.
I was facing with similar issue for 3 days. The below code helped me to get out of the issue.
string[] initialScopes = Configuration.GetValue<string>("CallApi:ScopeForAccessToken")?.Split(' ');
services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "AzureAd")
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddInMemoryTokenCaches();
services.AddControllers();
services.AddRazorPages().AddMvcOptions(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser().Build();
options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.SaveTokens = true; // this saves the token for the downstream api
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = async ctxt =>
{
ctxt.ProtocolMessage.RedirectUri = "https://example.org/signin-oidc";
await Task.Yield();
}
};
});

How do I call a web API on behalf of an AAD user using security claims?

So, to give you a general overview of what I am trying to achieve, I have a web application which
uses AAD authentication, and so users need to be signed in to a Microsoft organizational account in
order to use most of the controllers implemented in the web app (which targets .NET Core).
Visual Studio offers a template for this kind of web app setup. This
template project seems to obtain the user’s identity as a "ClaimsIdentity" (System.Security.Claims.ClaimsIdentity), which is ok so far, as long
as the user is AAD authenticated.
I also have a .NET Core Web API solution which the web app needs to make calls to on behalf of the
logged in user. So, I have a web app which signs in the user to AAD and then a web API (which the
web app calls) which has controller end points that expect an AAD authenticated request.
For this to work, my understanding is that the web app needs to include the signed in identity that
Microsoft, (which in this case is the security provider) provided it with, inside the header of the
request that it makes to the API. The API would then be able to view user claims and act accordingly.
The problem is here. As a header, I believe I need to provide the access token that Microsoft sends
to the web app.. however I cannot locate this token. All I can extract from User or User.Identity, are the claims. How
can I call a separate API on behalf of these claims? Do I need to completely disregard the template
that Microsoft provided and just make a call to the /token endpoint? I would just like to do this the right way :)
This is the ConfigureServices method in the web app Startup class:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
services.AddMvc(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
This is where I would like to call the external web API on behalf of the logged in AAD to get the
required data:
public IActionResult Index()
{
var user = User.Identity as ClaimsIdentity;
var request = (HttpWebRequest)WebRequest.Create("http://localhost:4110/data");
request.Headers["Authorization"] = "bearer " + getAccessToken_using_user;
var response = (HttpWebResponse)request.GetResponse();
var dataString = new StreamReader(response.GetResponseStream()).ReadToEnd();
return View();
}
Of course, my intention is to replace "getAccessToken_using_user" with the access token that Microsoft supposedly provides the web app with, as illustrated in their diagram.
You can use MSAL to get the access token for the downstream API.
https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/on-behalf-of#practical-usage-of-obo-in-an-aspnet--aspnet-core-application
This is a full example with on behalf flow:
https://github.com/Azure-Samples/active-directory-dotnet-native-aspnetcore-v2/tree/master/2.%20Web%20API%20now%20calls%20Microsoft%20Graph
public static IServiceCollection AddProtectedApiCallsWebApis(this IServiceCollection services, IConfiguration configuration, IEnumerable<string> scopes)
{
...
services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
{
options.Events.OnTokenValidated = async context =>
{
var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();
context.Success();
// Adds the token to the cache, and also handles the incremental consent and claim challenges
tokenAcquisition.AddAccountToCacheFromJwt(context, scopes);
await Task.FromResult(0);
};
});
return services;
}
private async Task GetTodoList(bool isAppStarting)
{
...
//
// Get an access token to call the To Do service.
//
AuthenticationResult result = null;
try
{
result = await _app.AcquireTokenSilent(Scopes, accounts.FirstOrDefault())
.ExecuteAsync()
.ConfigureAwait(false);
}
...
// Once the token has been returned by MSAL, add it to the http authorization header, before making the call to access the To Do list service.
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
// Call the To Do list service.
HttpResponseMessage response = await _httpClient.GetAsync(TodoListBaseAddress + "/api/todolist");
...
}

Cookie Authentication on controllers with Windows and Forms Login

Haven't found any really helpful articles or SO questions regarding this problem.
I want to behave my API in the following way:
All controller actions should only be available for cookie authenticated users
There are 2 login actions for getting a cookie by form (username/pw) or sso with the windows user (this is rather easy to do)
My problems with this currently:
When the API returns 401 (the correct status code) for a controller action, the browser automatically defaults to re-triggering the request with a NTLM token. The user doesn't have a cookie and is still able to call the API action because the windows authentication scheme is still registered somehow
It works as I want it to when I decorate ALL of the API actions with:
[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
In this case it seems that the Windows Authentication Scheme is never used but only cookies, which is what I want.
I want this to be the only allowed behavior though without having to decorate all of the actions explicitly and never allow Windows Authentication Scheme (apart from the windows login action)
I can also kind of getting it working when I make sure that the Cookie Authentication doesn't return 401 but returns 403, which doesn't make the browser to re-trigger the request with NTLM authentication. This is a workaround at best though.
The relevant parts of Startup.cs currently look like this:
services.AddAuthentication(opt =>
{
opt.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(ConfigureApplicationCookie);
Cookie config:
private static void ConfigureApplicationCookie(CookieAuthenticationOptions options)
{
options.Cookie.Name = Assembly.GetEntryAssembly().GetName().Name;
options.Cookie.HttpOnly = true;
options.Cookie.Expiration = TimeSpan.FromHours(1);
options.SlidingExpiration = true;
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
};
options.Events.OnRedirectToAccessDenied = context =>
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
return Task.CompletedTask;
};
options.LogoutPath = null;
options.LoginPath = null;
}
IIS (and IIS express) is configured to use Anonymous and Windows Authentication.

ASP.Net Core MVC/API/SignalR - Change authentication schemes (Cookie & JWT)

I've a .Net Core 2.2 web application MVC in which I've added API controllers and SignalR hubs. On the other side, I've a mobile app that calls the hub methods. Before calling hubs from the app, I am authenticating my users through an API call - getting back a JWT Token - and using this token for future requests, this way I can use Context.User.Identity.Name in my hub methods:
public static async Task<string> GetValidToken(string userName, string password)
{
using (var client = new HttpClient())
{
client.BaseAddress = new Uri(_API_BASE_URI);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
LoginViewModel loginVM = new LoginViewModel() { Email = userName, Password = password, RememberMe = false };
var formContent = Newtonsoft.Json.JsonConvert.SerializeObject(loginVM);
var content = new StringContent(formContent, Encoding.UTF8, "application/json");
HttpResponseMessage responseMessage;
try
{
responseMessage = await client.PostAsync("/api/user/authenticate", content);
var responseJson = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); ;
var jObject = JObject.Parse(responseJson);
_TOKEN = jObject.GetValue("token").ToString();
return _TOKEN;
}catch
[...]
Then using the token:
_connection = new HubConnectionBuilder().WithUrl(ApiCommunication._API_BASE_URI + "/network", options =>
{
options.AccessTokenProvider = () => Task.FromResult(token);
}).Build();
So far so good. It's working as expected on my mobile app. But in order to make it work I had to set this piece of code on server side (Startup.cs):
services.AddAuthentication(options =>
{
options .DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options .DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
...
This prevents me for using cookie authentication anymore and therefore the mvc web app is no more working as expected as it's not able to get the current authenticated user amongs requests.
Removing the lines:
options .DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options .DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
makes the web app working correctly but not the mobile app anymore (hub calls fail due to Context.User.Identity.Name equals to null).
I've been searching all around about how to handle different schemes (in my case cookie + jwt) and from my understanding, this is by design not possible anymore.
Is there any possible workaround to use double scheme or am I missing something?
I thought maybe I shoud host 2 separate projects instead and use one with Cookie authentication and the other one with JWT?
Thanks in advance.
There are multiple ways to solve the issue you encounter, but first let's go through why it's not currently working.
What DefaultAuthenticateScheme means
When you set a value to the DefaultAuthenticateScheme property of AuthenticationOptions, you instruct the authentication middleware to try and authenticate every single HTTP request against that specific scheme. I'm going to assume that you're using ASP.NET Identity for cookie-based authentication, and when you call AddIdentity, it registers the cookie authentication scheme as the default one for authentication purposes; you can see this in the source code on GitHub.
However, it doesn't mean you can't use any other authentication scheme in your application.
The authorization system default policy
If all the protected endpoints of your application are meant to be accessible to clients authenticated with cookies or JWTs, one option is to use the authorization system default policy. That special policy is used when you use "empty" instances of the AuthorizeAttribute class — either as an attribute to decorate controllers/actions, or globally at the app level with a new AuthorizeFilter(new AuthorizeAttribute()).
The default policy is set to only require an authenticated user, but doesn't define which authentication schemes need to be "tried" to authenticate the request. The result is that it relies on the authentication process already having been performed. It explains the behavior you're experiencing where only one of the 2 schemes works at a time.
We can change the default policy with a bit of code:
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("<your-cookie-authentication-scheme", "your-jwt-authentication-scheme")
.Build();
})
Specific authorization policies
If you find yourself in a situation where you require some endpoints to only be accessible to clients authenticated with cookies and others with JWTs, you can take advantage of authorization policies.
They work exactly like the default policy, expect you get to pick on an endpoint basis which one applies. You can add policies like so:
services.AddAuthorization(options =>
{
options.AddPolicy("Cookies", new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("<your-cookie-authentication-scheme")
.Build());
options.AddPolicy("JWT", new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("<your-jwt-authentication-scheme")
.Build());
})
You can then refer to these policies in appropriate endpoints by decorating them with [Authorize(Policy = "<policy-name>")]. As a side note, if the only differentiator between your policies is the authentication scheme, it's possible to achieve the same result without creating policies, and referring to the appropriate authentication scheme(s) in [Authorize] attributes with the AuthenticationSchemes property.
Policies are valuable when you have more complex rules, like that specific claim needs this specific value, for example.
I hope this helps, let me know how you go! 👍

Configure the authorization server endpoint

Question
How do we use a bearer token with ASP.NET 5 using a username and password flow? For our scenario, we want to let a user register and login using AJAX calls without needing to use an external login.
To do this, we need to have an authorization server endpoint. In the previous versions of ASP.NET we would do the following and then login at the ourdomain.com/Token URL.
// Configure the application for OAuth based flow
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14)
};
In the current version of ASP.NET, though, the above doesn't work. We've been trying to figure out the new approach. aspnet/identity example on GitHub, for instance, configures Facebook, Google, and Twitter authentication but does not appear to configure a non-external OAuth authorization server endpoint, unless that's what AddDefaultTokenProviders() does, in which case we're wondering what the URL to the provider would be.
Research
We've learned from reading the source here that we can add "bearer authentication middleware" to the HTTP pipeline by calling IAppBuilder.UseOAuthBearerAuthentication in our Startup class. This is a good start though we're still not sure of how to set its token endpoint. This didn't work:
public void Configure(IApplicationBuilder app)
{
app.UseOAuthBearerAuthentication(options =>
{
options.MetadataAddress = "meta";
});
// if this isn't here, we just get a 404
app.Run(async context =>
{
await context.Response.WriteAsync("Hello World.");
});
}
On going to ourdomain.com/meta we just receive our hello world page.
Further research showed that we can also use the IAppBuilder.UseOAuthAuthentication extension method, and that it takes a OAuthAuthenticationOptions parameter. That parameter has a TokenEndpoint property. So though we're not sure what we're doing, we tried this, which of course didn't work.
public void Configure(IApplicationBuilder app)
{
app.UseOAuthAuthentication("What is this?", options =>
{
options.TokenEndpoint = "/token";
options.AuthorizationEndpoint = "/oauth";
options.ClientId = "What is this?";
options.ClientSecret = "What is this?";
options.SignInScheme = "What is this?";
options.AutomaticAuthentication = true;
});
// if this isn't here, we just get a 404
app.Run(async context =>
{
await context.Response.WriteAsync("Hello World.");
});
}
In other words, in going to ourdomain.com/token, there is no error there is just again our hello world page.
EDIT (01/28/2021): AspNet.Security.OpenIdConnect.Server has been merged into OpenIddict as part of the 3.0 update. To get started with OpenIddict, visit documentation.openiddict.com.
Okay, let's recap the different OAuth2 middleware (and their respective IAppBuilder extensions) that were offered by OWIN/Katana 3 and the ones that will be ported to ASP.NET Core:
app.UseOAuthBearerAuthentication/OAuthBearerAuthenticationMiddleware: its name was not terribly obvious, but it was (and still is, as it has been ported to ASP.NET Core) responsible for validating access tokens issued by the OAuth2 server middleware. It's basically the token counterpart of the cookies middleware and is used to protect your APIs. In ASP.NET Core, it has been enriched with optional OpenID Connect features (it is now able to automatically retrieve the signing certificate from the OpenID Connect server that issued the tokens).
Note: starting with ASP.NET Core beta8, it is now namedapp.UseJwtBearerAuthentication/JwtBearerAuthenticationMiddleware.
app.UseOAuthAuthorizationServer/OAuthAuthorizationServerMiddleware: as the name suggests, OAuthAuthorizationServerMiddleware was an OAuth2 authorization server middleware and was used to create and issue access tokens. This middleware won't be ported to ASP.NET Core: OAuth Authorization Service in ASP.NET Core.
app.UseOAuthBearerTokens: this extension didn't really correspond to a middleware and was simply a wrapper around app.UseOAuthAuthorizationServer and app.UseOAuthBearerAuthentication. It was part of the ASP.NET Identity package and was just a convenient way to configure both the OAuth2 authorization server and the OAuth2 bearer middleware used to validate access tokens in a single call. It won't be ported to ASP.NET Core.
ASP.NET Core will offer a whole new middleware (and I'm proud to say I designed it):
app.UseOAuthAuthentication/OAuthAuthenticationMiddleware: this new middleware is a generic OAuth2 interactive client that behaves exactly like app.UseFacebookAuthentication or app.UseGoogleAuthentication but that supports virtually any standard OAuth2 provider, including yours. Google, Facebook and Microsoft providers have all been updated to inherit from this new base middleware.
So, the middleware you're actually looking for is the OAuth2 authorization server middleware, aka OAuthAuthorizationServerMiddleware.
Though it is considered as an essential component by a large part of the community, it won't be ported to ASP.NET Core.
Luckily, there's already a direct replacement: AspNet.Security.OpenIdConnect.Server (https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server)
This middleware is an advanced fork of the OAuth2 authorization server middleware that comes with Katana 3 but that targets OpenID Connect (which is itself based on OAuth2). It uses the same low-level approach that offers a fine-grained control (via various notifications) and allows you to use your own framework (Nancy, ASP.NET Core MVC) to serve your authorization pages like you could with the OAuth2 server middleware. Configuring it is easy:
ASP.NET Core 1.x:
// Add a new middleware validating access tokens issued by the server.
app.UseOAuthValidation();
// Add a new middleware issuing tokens.
app.UseOpenIdConnectServer(options =>
{
options.TokenEndpointPath = "/connect/token";
// Create your own `OpenIdConnectServerProvider` and override
// ValidateTokenRequest/HandleTokenRequest to support the resource
// owner password flow exactly like you did with the OAuth2 middleware.
options.Provider = new AuthorizationProvider();
});
ASP.NET Core 2.x:
// Add a new middleware validating access tokens issued by the server.
services.AddAuthentication()
.AddOAuthValidation()
// Add a new middleware issuing tokens.
.AddOpenIdConnectServer(options =>
{
options.TokenEndpointPath = "/connect/token";
// Create your own `OpenIdConnectServerProvider` and override
// ValidateTokenRequest/HandleTokenRequest to support the resource
// owner password flow exactly like you did with the OAuth2 middleware.
options.Provider = new AuthorizationProvider();
});
There's an OWIN/Katana 3 version, and an ASP.NET Core version that supports both .NET Desktop and .NET Core.
Don't hesitate to give the Postman sample a try to understand how it works. I'd recommend reading the associated blog post, that explains how you can implement the resource owner password flow.
Feel free to ping me if you still need help.
Good luck!
With #Pinpoint's help, we've wired together the rudiments of an answer. It shows how the components wire together without being a complete solution.
Fiddler Demo
With our rudimentary project setup, we were able to make the following request and response in Fiddler.
Request
POST http://localhost:50000/connect/token HTTP/1.1
User-Agent: Fiddler
Host: localhost:50000
Content-Length: 61
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=my_username&password=my_password
Response
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 1687
Content-Type: application/json;charset=UTF-8
Expires: -1
X-Powered-By: ASP.NET
Date: Tue, 16 Jun 2015 01:24:42 GMT
{
"access_token" : "eyJ0eXAiOi ... 5UVACg",
"expires_in" : 3600,
"token_type" : "bearer"
}
The response provides a bearer token that we can use to gain access to the secure part of the app.
Project Structure
This is the structure of our project in Visual Studio. We had to set its Properties > Debug > Port to 50000 so that it acts as the identity server that we configured. Here are the relevant files:
ResourceOwnerPasswordFlow
Providers
AuthorizationProvider.cs
project.json
Startup.cs
Startup.cs
For readability, I've split the Startup class into two partials.
Startup.ConfigureServices
For the very basics, we only need AddAuthentication().
public partial class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication();
}
}
Startup.Configure
public partial class Startup
{
public void Configure(IApplicationBuilder app)
{
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear();
// Add a new middleware validating access tokens issued by the server.
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
Audience = "resource_server_1",
Authority = "http://localhost:50000/",
RequireHttpsMetadata = false
});
// Add a new middleware issuing tokens.
app.UseOpenIdConnectServer(options =>
{
// Disable the HTTPS requirement.
options.AllowInsecureHttp = true;
// Enable the token endpoint.
options.TokenEndpointPath = "/connect/token";
options.Provider = new AuthorizationProvider();
// Force the OpenID Connect server middleware to use JWT
// instead of the default opaque/encrypted format.
options.AccessTokenHandler = new JwtSecurityTokenHandler
{
InboundClaimTypeMap = new Dictionary<string, string>(),
OutboundClaimTypeMap = new Dictionary<string, string>()
};
// Register an ephemeral signing key, used to protect the JWT tokens.
// On production, you'd likely prefer using a signing certificate.
options.SigningCredentials.AddEphemeralKey();
});
app.UseMvc();
app.Run(async context =>
{
await context.Response.WriteAsync("Hello World!");
});
}
}
AuthorizationProvider.cs
public sealed class AuthorizationProvider : OpenIdConnectServerProvider
{
public override Task ValidateTokenRequest(ValidateTokenRequestContext context)
{
// Reject the token requests that don't use
// grant_type=password or grant_type=refresh_token.
if (!context.Request.IsPasswordGrantType() &&
!context.Request.IsRefreshTokenGrantType())
{
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
description: "Only grant_type=password and refresh_token " +
"requests are accepted by this server.");
return Task.FromResult(0);
}
// Since there's only one application and since it's a public client
// (i.e a client that cannot keep its credentials private), call Skip()
// to inform the server that the request should be accepted without
// enforcing client authentication.
context.Skip();
return Task.FromResult(0);
}
public override Task HandleTokenRequest(HandleTokenRequestContext context)
{
// Only handle grant_type=password token requests and let the
// OpenID Connect server middleware handle the other grant types.
if (context.Request.IsPasswordGrantType())
{
// Validate the credentials here (e.g using ASP.NET Core Identity).
// You can call Reject() with an error code/description to reject
// the request and return a message to the caller.
var identity = new ClaimsIdentity(context.Options.AuthenticationScheme);
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "[unique identifier]");
// By default, claims are not serialized in the access and identity tokens.
// Use the overload taking a "destinations" parameter to make sure
// your claims are correctly serialized in the appropriate tokens.
identity.AddClaim("urn:customclaim", "value",
OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
context.Options.AuthenticationScheme);
// Call SetResources with the list of resource servers
// the access token should be issued for.
ticket.SetResources("resource_server_1");
// Call SetScopes with the list of scopes you want to grant
// (specify offline_access to issue a refresh token).
ticket.SetScopes("profile", "offline_access");
context.Validate(ticket);
}
return Task.FromResult(0);
}
}
project.json
{
"dependencies": {
"AspNet.Security.OpenIdConnect.Server": "1.0.0",
"Microsoft.AspNetCore.Authentication.JwtBearer": "1.0.0",
"Microsoft.AspNetCore.Mvc": "1.0.0",
}
// other code omitted
}

Categories