OWIN token authentication 400 Bad Request on OPTIONS from browser - asp.net

I am using token authentication for small project based on this article: http://bitoftech.net/2014/06/09/angularjs-token-authentication-using-asp-net-web-api-2-owin-asp-net-identity/
Everything seems to work fine except one thing: OWIN based token authentication doesn't allow OPTIONS request on /token endpoint. Web API returns 400 Bad Request and whole browser app stops sending POST request to obtain token.
I have all CORS enabled in application as in sample project. Below some code that might be relevant:
public class Startup
{
public static OAuthBearerAuthenticationOptions OAuthBearerOptions { get; private set; }
public void Configuration(IAppBuilder app)
{
AreaRegistration.RegisterAllAreas();
UnityConfig.RegisterComponents();
GlobalConfiguration.Configure(WebApiConfig.Register);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
HttpConfiguration config = new HttpConfiguration();
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
ConfigureOAuth(app);
WebApiConfig.Register(config);
app.UseWebApi(config);
Database.SetInitializer(new ApplicationContext.Initializer());
}
public void ConfigureOAuth(IAppBuilder app)
{
//use a cookie to temporarily store information about a user logging in with a third party login provider
app.UseExternalSignInCookie(Microsoft.AspNet.Identity.DefaultAuthenticationTypes.ExternalCookie);
OAuthBearerOptions = new OAuthBearerAuthenticationOptions();
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(60),
Provider = new SimpleAuthorizationServerProvider(),
RefreshTokenProvider = new SimpleRefreshTokenProvider()
};
// Token Generation
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthentication(OAuthBearerOptions);
}
}
Below is my login function from javascript (I am using angularjs for that purpose)
var _login = function (loginData) {
var data = "grant_type=password&username=" + loginData.userName + "&password=" + loginData.password;
data = data + "&client_id=" + ngAuthSettings.clientId;
var deferred = $q.defer();
$http.post(serviceBase + 'token', data, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }).success(function (response) {
localStorageService.set('authorizationData', { token: response.access_token, userName: loginData.userName, refreshToken: response.refresh_token, useRefreshTokens: true });
_authentication.isAuth = true;
_authentication.userName = loginData.userName;
_authentication.useRefreshTokens = loginData.useRefreshTokens;
deferred.resolve(response);
}).error(function (err, status) {
_logOut();
deferred.reject(err);
});
return deferred.promise;
};
var _logOut = function () {
localStorageService.remove('authorizationData');
_authentication.isAuth = false;
_authentication.userName = "";
_authentication.useRefreshTokens = false;
};

I've lost some time on this problem today. Finally i think i've found a solution.
Override method inside your OAuthAuthorizationServerProvider:
public override Task MatchEndpoint(OAuthMatchEndpointContext context)
{
if (context.IsTokenEndpoint && context.Request.Method == "OPTIONS")
{
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Headers", new[] { "authorization" });
context.RequestCompleted();
return Task.FromResult(0);
}
return base.MatchEndpoint(context);
}
This appears to do three necessary things:
Force auth server to respond to OPTIONS request with 200 (OK) HTTP status,
Allow request to come from anywhere by setting Access-Control-Allow-Origin
Allows Authorization header to be set on subsequent requests by setting Access-Control-Allow-Headers
After those steps angular finally behaves correctly when requesting token endpoint with OPTIONS method. OK status is returned and it repeats request with POST method to get full token data.

Override this method inside your OAuthAuthorizationServerProvider:
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
context.Validated();
}

Are you running it locally or are you publishing it to Azure like in the blog article's sample code?
If you're running it on Azure, you can easily fix CORS problems by enabling CORS in the Azure portal:
Click on your App Service in the Azure Portal to enter the management screen.
In the list of management options, scroll down to the 'API' section, where you will find the 'CORS' option. (Alternatively type 'CORS' in the search box).
Enter the allowed origin, or enter '*' to enable all, and click save.
This fixed the OPTIONS preflight check problem for me, which a few other people seem to have had from the code in that particular blog article.

Solved it. The problem was not sending with OPTIONS request header Access-Control-Request-Method

This should do the trick:
app.UseCors(CorsOptions.AllowAll);

Related

SignalR gives 404 when using WebSockets or ServerSentEvents for some users but not for others

SignalR gives me 404 when trying to connect for some users. URLs are the same except for access_token.
It is stable reproducible per user (I mean that some users are stable OK, some users are stable 404).
access_token parsed jwt diff (left is OK user, right gets 404):
I did a trace level of logs and have next:
For the OK user:
For the user that gets 404:
Note: URLs under black squares are the same.
Front End is Angular 9 with package "#microsoft/signalr": "^3.1.8", and here's the code that builds the connection:
private buildHub(): HubConnection {
console.log(this.authService.accessToken);
let builder = new HubConnectionBuilder()
.withAutomaticReconnect()
.configureLogging(LogLevel.Information)
.withUrl('ws/notificationHub', {
accessTokenFactory: () => this.authService.accessToken
});
if (this.debugMode) {
builder = builder.configureLogging(LogLevel.Trace);
}
return builder.build();
}
Backend is using next code in Startup for configuring signalR hub:
In public void ConfigureServices(IServiceCollection services):
services.AddSignalR()
.AddJsonProtocol(options =>
{
options.PayloadSerializerSettings.ContractResolver = new DefaultContractResolver();
});
In public void Configure(IApplicationBuilder app, IHostingEnvironment env):
app.UseSignalR(route =>
{
route.MapHub<NotificationHub>("/ws/notificationHub");
});
Also we use custom authentication, so we have Authorize attribute for the Hub class:
[Authorize]
public class NotificationHub: Hub<INotificationHubClient>
and this code in public void ConfigureServices(IServiceCollection services):
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = identityServerSettings.Url;
options.Audience = identityServerSettings.ApiScopeName;
options.RequireHttpsMetadata = identityServerSettings.RequireHttpsMetadata;
options.Events = new Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/ws"))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
Unfortunately, I don't have the full access to the environment where it is reproducible, but I can request to see any settings or try to make some changes.
What else can I try to troubleshoot the issue?
UPDATE: negotiate is fine for both users.
I had this issue recently, after the size of my JWT increased. I found that in my case the 404 error was being thrown by IIS because the query string exceeded the limit of 2048. After increasing the query string max length, my issue was resolved.

Microsoft sign in works without redirect url

When I sign in with Microsoft OAuth in my Blazor app, authenticateResult.Succeeded is true, even if I don't specify a redirect URI. It's failing as intended for Google, if I don't add my URI to the OAuth client.
Imo it shouldn't work without that redirect URI, according to the OAuth2.0 spec:
The authorization server MUST require public clients and SHOULD
require confidential clients to register their redirection URIs.
I'm using Microsoft.AspNetCore.Authentication.MicrosoftAccount 3.0.3 with .NET Core 3.0
public class ExternalLoginModel : PageModel
{
public IActionResult OnGetAsync(string externalAuthType, string returnUrl)
{
var authenticationProperties = new AuthenticationProperties
{
RedirectUri = Url.Page("./externallogin",
pageHandler: "Callback",
values: new { returnUrl }),
};
return new ChallengeResult(externalAuthType, authenticationProperties);
}
public async Task<IActionResult> OnGetCallbackAsync(
string returnUrl = null, string remoteError = null)
{
var authenticateResult = await HttpContext.AuthenticateAsync("External");
if (!authenticateResult.Succeeded) // Should be false for Microsoft sign in
return BadRequest();
...
return LocalRedirect(returnUrl);
}
}
With the following added to my Startup:
services.AddAuthentication(o =>
{
o.DefaultSignInScheme = "External";
}).AddCookie("External");
services.AddAuthentication().AddGoogle(google =>
{
google.ClientId = Configuration["Authentication:Google:ClientId"];
google.ClientSecret = Configuration["Authentication:Google:ClientSecret"];
});
services.AddAuthentication().AddMicrosoftAccount(microsoftOptions =>
{
microsoftOptions.ClientId = Configuration["Authentication:Microsoft:ClientId"];
microsoftOptions.ClientSecret = Configuration["Authentication:Microsoft:ClientSecret"];
});
My App's Authentication settings look like this (I'm actually using localhost:12345 in the settings, but that's not what my app is running on..):
Ironically the last sentence might explain it, but I don't even know which flow the MicrosoftAccount library is using and I only get generic documentation when googling.
It fails as intended when using a completely different domain, not localhost with different ports. I guess that's good enough.
Additionally I unchecked "ID token" and "Treat application as a public client", therefore Authorization code flow should be used, to my understanding.

Can't enable CORS for web api

I tried everything but can't enable CORS for my WebApi project. I guess I 'm missing something or not doing it right.
My StartUp.config is:
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
// Web API routes
//app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
config.MapHttpAttributeRoutes();
config.EnableCors(new System.Web.Http.Cors.EnableCorsAttribute("http://www.test.ca", "*", "GET,POST")); //enable only for this domain
ConfigureOAuth(app);
app.UseWebApi(config);
ConfigureAutofac(app, config);
}
My api controller:
[HttpPost]
[Authorize]
[Route("api/Accounts/GetTestTest")]
[System.Web.Http.Cors.EnableCors("http://www.test.ca", "*", "*")]
public HttpResponseMessage GetTestTest()
{
return this.Request.CreateResponse(System.Net.HttpStatusCode.OK);
}
Here I should be restricted because my request are made from MVC application which runs on localhost. Also I'm using tokens to authorize users.
Any ideas what I am missing or doing wrong?
EDIT Request is comming from MVC controller action like this:
static string CallApi(string url, string token, LogInRequest request)
{
System.Net.ServicePointManager.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;
using (var client = new HttpClient())
{
if (!string.IsNullOrWhiteSpace(token))
{
var t = Newtonsoft.Json.JsonConvert.DeserializeObject<CashManager.Models.Global.Token>(token);
client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + t.access_token);
}
var response = client.PostAsJsonAsync<string>(url,string.Empty).Result;
return response.Content.ReadAsStringAsync().Result;
}
}
CORS does not apply to requests made from a back-end. It only applies to requests coming from browsers via AJAX.
You will need to do IP address-based filtering or something else to block requests from certain places. The authentication you have might be good enough though.

OWIN Oauth differentiate expired and invalid token

I use OWIN Oauth in my ASP.NET MVC application to provide access token for mobile applications. Here's the setup of OAuth:
app.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/api/authenticate/login"),
Provider = dependencyContainer.GetService<IOAuthAuthorizationServerProvider>(),
RefreshTokenProvider = dependencyContainer.GetService<IAuthenticationTokenProvider>(),
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(applicationSettings.AccessTokenLifeTimeInMinutes),
AllowInsecureHttp = true
});
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
I also have custom provider and custom refresh token provider as you can see above. Everything is working fine, when a request from mobile is expired or invalid, I use a custom AuthorizeAttribute to return a json with message "unauthorized"
public class ApiAuthorizeAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
filterContext.Result = new JsonResult
{
Data = new
{
success = false,
error = "Unauthorized"
},
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
}
}
However in one scenario, the mobile applications need to differentiate the response from server for 2 cases: access token is expired, or access token is invalid (.e.g. modified in the middle). I'm not sure how I can implement that requirement. I tried to create a custom access token provider, inheriting from AuthenticationTokenProvider, register it in UseOAuthAuthorizationServer() above, but both Receive() and ReceiveAsync() are not called when server receives access token from mobile
Solved the issue. My approach of creating custom access token provider works. Initially I registered it with UseOAuthAuthorizationServer(), but it should be registered using UseOAuthBearerAuthentication() instead
Here's my custom class, in case anyone needs:
public class CustomAccessTokenProvider : AuthenticationTokenProvider
{
public override void Receive(AuthenticationTokenReceiveContext context)
{
context.DeserializeTicket(context.Token);
var expired = context.Ticket.Properties.ExpiresUtc < DateTime.UtcNow;
if (expired)
{
//If current token is expired, set a custom response header
context.Response.Headers.Add("X-AccessTokenExpired", new string[] { "1" });
}
base.Receive(context);
}
}
Register it when setting up OWIN OAuth:
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
{
AccessTokenProvider = new CustomAccessTokenProvider()
});

Disable GET functionality of WEB API token call

I developed an application using ASP.NET WEB API 2. The application is completed and in the process of having security review done on it, but one of the requirements is that any GET requests for login must be disabled.
We are making the call to the token action over POST, but the security team picked up that you can still make the same request with GET and that needs to be removed. I know the token call is one that is built into the whole OWIN/OAUTH system, but is it possible to configure it so that it will only accept POST requests and block GET?
Thanks in advance.
By looking into Katana project sources I can see that in Microsoft.Owin.Security.OAuth.OAuthAuthorizationServerHandler they have the following check:
if (Options.TokenEndpointPath.HasValue && Options.TokenEndpointPath == Request.Path)
{
matchRequestContext.MatchesTokenEndpoint();
}
As you can see there is no additional check for HTTP METHOD. Therefore as one of the possible solution I can propose you to write your own middleware which is executing before authentication one and checks for the HTTP METHOD:
public class OnlyPostTokenMiddleware : OwinMiddleware
{
private readonly OAuthAuthorizationServerOptions opts;
public OnlyPostTokenMiddleware(OwinMiddleware next, OAuthAuthorizationServerOptions opts) : base(next)
{
this.opts = opts;
}
public override Task Invoke(IOwinContext context)
{
if (opts.TokenEndpointPath.HasValue && opts.TokenEndpointPath == context.Request.Path && context.Request.Method == "POST")
{
return Next.Invoke(context);
}
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
context.Response.ReasonPhrase = "Not Found";
return context.Response.WriteAsync("Not Found");
}
}
then in Startup.cs you would have something similar to:
var authOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/token"),
Provider = Resolver.GetService<OAuthProvider>(),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1)
};
app.Use<OnlyPostTokenMiddleware>(authOptions);
app.UseOAuthAuthorizationServer(authOptions);

Resources