I have Asp.Net core backend running in Azure.
HTML/JS frontend running on localhost, using CORS to communicate with backend
When both, frontend and backend are in localhost, or they are both in Azure, the authentication works -> Azure AD app is setup correctly.
Here is how I log in:
[Route("/api/[controller]")]
public class AccountController : Controller
{
[HttpGet]
public IActionResult Index()
{
return Json(new AccountInfoViewModel
{
IsAuthenticated = User.Identity.IsAuthenticated,
UserName = User.Identity.Name,
Roles = new string[0],
LoginUrl = Url.Action(nameof(Login), null, null, null, Request.Host.Value),
LogoutUrl = Url.Action(nameof(Login), null, null, null, Request.Host.Value),
});
}
[HttpGet("login")]
public async Task Login(string returnUrl)
{
await HttpContext.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = returnUrl });
}
[HttpGet("logoff")]
public async Task LogOff()
{
await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
[HttpGet("endsession")]
public async Task EndSession()
{
// If AAD sends a single sign-out message to the app, end the user's session, but don't redirect to AAD for sign out.
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
}
so from localhost, I then redirect to:
https://myapp.azurewebsites.net/Account/Login?returnUrl=localhost:12345
which triggers Login action and it redirects me to my Azure AD SSO page and after login, it redirects me back to localhost. However, the request to the backend is still not authenticated.
Important:
When I remove redirectUrl from login action, I'm redirected to the backend root instead of original origin (localhost). Any request from that origin (backend) is authenticated.
I had to explicitelly tell javascript to include authentication headers:
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
for more details: https://learn.microsoft.com/en-us/aspnet/core/security/cors#credentials-in-cross-origin-requests
EDIT: in case of chrome and opera, which implements SameSite attribute of cookies, you also have to setup authentication cookie like this:
services.AddAuthentication(...)
.AddCookie(option => option.Cookie.SameSite = SameSiteMode.None)
.AddOpenIdConnect(...)
Related
I made the login method like this:
public async Task<IActionResult> Login([FromBody] LoginUserDTO userDTO)
{
var res = await _authManager.ValidateUser(userDTO);
if (!res) return Unauthorized();
await _authManager.SetLoginInfo(userDTO, Request);
return Accepted(new { Token = await _authManager.CreateToken() });
}
public async Task<string> CreateToken()
{
var signingCredentials = GetSigningCredentials();
var claims = await GetClaims();
var token = GenerateTokenOptions(signingCredentials, claims);
return new JwtSecurityTokenHandler().WriteToken(token);
}
How can I create an endpoint for Logout?
In ASP, there is no such thing as logging out from a JWT on the server.
A JWT is a token that has an expiry date and is issued by the server (or a trusted third-party). It is then cached by the client and sent to the server by the client in the header of subsequent requests and is then validated by the server to ensure that it is both valid and not expired.
If the expiry is reached, then the server will return a 401 - Unauthorised response.
If you want to log a client out then you just remove the client side cached token so that it cannot be sent in the header of any future requests.
So I decided to create a simple web app which sends a login request to my login endpoint on my Web API like this.
await fetch("https://localhost:1234/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ Username: "Admin", Password: "Password" }),
})
.then((response) => response.json())
.then((jsondata) => {
console.log(jsondata);
});
}
And what happens on my Web API is this
public IActionResult Login([FromBody] UserLogin userLogin)
{
var user = Authenticate(userLogin);
if (user != null)
{
var token = Generate(user);
Response.Cookies.Append("Authorization", token, new CookieOptions { HttpOnly = true, Secure = true });
return Ok();
}
return NotFound("User not found.");
}
So when the user logs in with a valid username and password, it generates a JWT and adds it to the response cookies. When I make a request with Postman/Insomnia, I can see the cookie in the response which means that on the client, when I make the same request I should get the same response. However, the issue is that I have no idea how to retrieve that cookie and store it so that I can send it in later requests when trying to access pages that require a JWT to authorize.
When I check the browsers and I got to Application > Cookies there is nothing there.
The goal is to be able to receive it, store is as a httpOnly cookie (because that's that's slightly safer than storing it on session/localStorage I've read) and then be able to use it to access other pages which requires me to send it to authorize.
That seems to be an issue because you can read or write httpOnly cookies using JavaScript https://stackoverflow.com/a/14691716/5693405
What's the proper way of dealing with this?
You can create a WEB API method Login to return token:
[Route("api/auth")]
[ApiController]
public class AuthController : ControllerBase
{
// GET api/values
[HttpPost, Route("login")]
public IActionResult Login([FromBody]LoginModel user)
{
// ... other code is omitted for the brevity
return Ok(new { Token = tokenString });
}
}
and then you can just store token at local storage of your browser:
login(form: NgForm) {
// ... other code is omitted for the brevity
localStorage.setItem("jwt", token);
// ... other code is omitted for the brevity
}
I have setup Asp.Net Identity in my application, and would like to have the following setup:
Have a selfmade username&password authentication as IdentityConstants.ApplicationScheme
Have external providers (e.g. Google, Facebook) which authenticate as IdentityConstants.ExternalScheme
My social login looks like this:
[HttpGet]
[Route("ExternalLogin")]
public IActionResult ExternalLogin([FromQuery] string provider, [FromQuery] bool rememberMe)
{
var redirectUrl = Url.Action("SocialLoginCallback", "SocialLogin", new
{
rememberMe
});
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
return Challenge(properties, provider);
}
[HttpGet]
[Route("SocialLoginCallback")]
public async Task<IActionResult> SocialLoginCallback([FromQuery] bool rememberMe)
{
// Grab the external login information from the http context
var loginInfo = await _signInManager.GetExternalLoginInfoAsync();
if (loginInfo is null)
{
return Problem();
}
var signinResult = await _signInManager.ExternalLoginSignInAsync(loginInfo.LoginProvider, loginInfo.ProviderKey, rememberMe, true);
if (signinResult.Succeeded)
{
return Ok();
}
return Ok();
}
My startup google auth looks like:
authBuilder.AddGoogle(options =>
{
options.ClientId = Configuration["GoogleClientId"];
options.ClientSecret = Configuration["GoogleClientSecret"];
options.SignInScheme = IdentityConstants.ExternalScheme;
});
However, I noticed that the login is still executed with IdentityConstants.ApplicationScheme.
After examining why, the problem seems to stem from the var signinResult = await _signInManager.ExternalLoginSignInAsync(loginInfo.LoginProvider, loginInfo.ProviderKey, rememberMe, true); call. Internally this runs into a call to await Context.SignInAsync(IdentityConstants.ApplicationScheme, userPrincipal, authenticationProperties ?? new AuthenticationProperties());, which has the ApplicationScheme hardcoded.
How can I get this to work as I would like to have?
I would like to distinguish both logins in a middleware, and as of now, the only way to do this would be to check the respective claims, but I'd rather just to a simple check which auth scheme the login is using. Is this possible?
If you want to set the default authentication method ,You can set DefaultScheme in ConfigureServices/Startup.cs.
public void ConfigureServices(IServiceCollection services)
{
//......
services.AddAuthentication(option =>
{
option.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(option =>
{
option.LoginPath = "/Identity/Account/Login";
})
.AddGoogle(option => {
option.ClientId = "xxx";
option.ClientSecret = "xxx";
});
}
I set the login page as the default Identity login page, when I access the action whitch has [Authorize] attribute , the login page has two ways to log in.
If the app have multiple instances of an authentication handler , You can use [Authorize(AuthenticationSchemes = xxxx)] to Select the scheme with the Authorize attribute.
You can read this document to learn more details about Authorize with a specific scheme in ASP.NET Core
I have a problem enabling Facebook auth in my ASP.NET Core web app. I'm using ASP.NET Core Authentication but not Identity. The auth is configured in Startup like this:
services
.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddFacebook(options =>
{
options.ClientId = "clientid";
options.ClientSecret = "secret";
options.CallbackPath = "/signinfacebookcallback";
});
As shown in the code, I want to use cookie auth, but also allow people to sign in with Facebook. Once they have been successfully signed in, I want to set the auth cookie.
To show the challenge, I have the following action:
[HttpGet]
[Route("signinfacebook")]
public ActionResult SignInFacebook()
{
return Challenge(FacebookDefaults.AuthenticationScheme);
}
This redirects the user to the Facebook login screen. Once they sign in, the user is redirected to the URL specified in config:
[HttpGet]
[Route("signinfacebookcallback")]
public async Task<ActionResult> SignInFacebookCallback()
{
var result = await HttpContext.AuthenticateAsync();
if (!result.Succeeded) return Redirect("/login/");
...
}
When I debug the code, result.Succeeded returns false and the AuthenticationResult object doesn't contain more information on why Succeeded is false.
I verified that the app id and secret are correct.
What could be the issue here?
The CallbackPath in the OpenID Connect middleware is internal path that are used for the authentication flow of the OpenID Connect protocol. After Identity provider redirect user to that url in your application , middeware will handle token valiation ,token decode,exchange token and finally fill the user principle , and that process is fired before your controller gets involved .
Since CallbackPath is internal and will handled by OpenID Connect middleware automatically , you don't need to care about , make sure the callback is registered in facebook's allowed redirect url and let middleware handle the callback .If you want to redirect user to specific route/page after authentication , put the url to AuthenticationProperties :
if (!User.Identity.IsAuthenticated)
{
return Challenge(new AuthenticationProperties() { RedirectUri = "/home/Privacy" } ,FacebookDefaults.AuthenticationScheme);
}
And you should remove the callback path route (signinfacebookcallback) in your application .
UPDATE
If you want to access database and manage local user , you can use built-in events in middleware, for AddFacebook middleware , you can use OnTicketReceived to add access database , manage users and add claims to user's princple :
.AddFacebook(options =>
{
options.ClientId = "xxxxxxxxxxxxx";
options.ClientSecret = "xxxxxxxxxxxxxxxxxxxx";
options.CallbackPath = "/signinfacebookcallback";
options.Events = new OAuthEvents
{
OnTicketReceived = ctx =>
{
//query the database to get the role
var db = ctx.HttpContext.RequestServices.GetRequiredService<YourDbContext>();
// add claims
var claims = new List<Claim>
{
new Claim(ClaimTypes.Role, "Admin")
};
var appIdentity = new ClaimsIdentity(claims);
ctx.Principal.AddIdentity(appIdentity);
return Task.CompletedTask;
},
};
});
I have implemented a Basic Authentication Middleware for Katana (Code below).
(My client is hosted on a cross domain then the actually API).
The browser can skip the preflight request if the following conditions
are true:
The request method is GET, HEAD, or POST, and The application does not
set any request headers other than Accept, Accept-Language,
Content-Language, Content-Type, or Last-Event-ID, and The Content-Type
header (if set) is one of the following:
application/x-www-form-urlencoded multipart/form-data text/plain
In javascript I set the authentication header( with jquery, beforeSend) on all requests for the server to accept the requests. This means that above will send the Options request on all requests. I dont want that.
function make_base_auth(user, password) {
var tok = user + ':' + password;
var hash = Base64.encode(tok);
return "Basic " + hash;
}
What would I do to get around this? My idea would be to have the user information stored in a cookie when he has been authenticated.
I also saw in the katana project that are a Microsoft.Owin.Security.Cookies - is this maybe what i want instead of my own basic authentication?
BasicAuthenticationMiddleware.cs
using Microsoft.Owin;
using Microsoft.Owin.Logging;
using Microsoft.Owin.Security.Infrastructure;
using Owin;
namespace Composite.WindowsAzure.Management.Owin
{
public class BasicAuthenticationMiddleware : AuthenticationMiddleware<BasicAuthenticationOptions>
{
private readonly ILogger _logger;
public BasicAuthenticationMiddleware(
OwinMiddleware next,
IAppBuilder app,
BasicAuthenticationOptions options)
: base(next, options)
{
_logger = app.CreateLogger<BasicAuthenticationMiddleware>();
}
protected override AuthenticationHandler<BasicAuthenticationOptions> CreateHandler()
{
return new BasicAuthenticationHandler(_logger);
}
}
}
BasicAuthenticationHandler.cs
using Microsoft.Owin.Logging;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Infrastructure;
using System;
using System.Text;
using System.Threading.Tasks;
namespace Composite.WindowsAzure.Management.Owin
{
public class BasicAuthenticationHandler : AuthenticationHandler<BasicAuthenticationOptions>
{
private readonly ILogger _logger;
public BasicAuthenticationHandler(ILogger logger)
{
_logger = logger;
}
protected override Task ApplyResponseChallengeAsync()
{
_logger.WriteVerbose("ApplyResponseChallenge");
if (Response.StatusCode != 401)
{
return Task.FromResult<object>(null);
}
AuthenticationResponseChallenge challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode);
if (challenge != null)
{
Response.Headers.Set("WWW-Authenticate", "Basic");
}
return Task.FromResult<object>(null);
}
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
{
_logger.WriteVerbose("AuthenticateCore");
AuthenticationProperties properties = null;
var header = Request.Headers["Authorization"];
if (!String.IsNullOrWhiteSpace(header))
{
var authHeader = System.Net.Http.Headers.AuthenticationHeaderValue.Parse(header);
if ("Basic".Equals(authHeader.Scheme, StringComparison.OrdinalIgnoreCase))
{
string parameter = Encoding.UTF8.GetString(Convert.FromBase64String(authHeader.Parameter));
var parts = parameter.Split(':');
if (parts.Length != 2)
return null;
var identity = await Options.Provider.AuthenticateAsync(userName: parts[0], password: parts[1], cancellationToken: Request.CallCancelled);
return new AuthenticationTicket(identity, properties);
}
}
return null;
}
}
}
Options.Provider.AuthenticateAsync validated the username/password and return the identity if authenticated.
Specifications
What I am trying to solve is: I have a Owin Hosted WebAPI deployed with N Azure Cloud Services. Each of them are linked to a storage account that holds a list of username/hashed passwords.
From my client I am adding any of these N services to the client and can then communicate with them by their webapis. They are locked down with authentication. The first step is to validate the users over basic authentication scheme with the list provided above. After that, I hope its easy to add other authentication schemes very easy as of the Owin, UseWindowsAzureAuthentication ect, or UseFacebookAuthentication. (I do have a challenge here, as the webapi do not have web frontend other then the cross domain site that adds the services).
If your good at Katana and want to work alittle with me on this, feel free to drop me a mail at pks#s-innovations.net. I will provide the answer here at the end also.
Update
Based on answer I have done the following:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Application",
AuthenticationMode = AuthenticationMode.Active,
LoginPath = "/Login",
LogoutPath = "/Logout",
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = context =>
{
// context.RejectIdentity();
return Task.FromResult<object>(null);
},
OnResponseSignIn = context =>
{
}
}
});
app.SetDefaultSignInAsAuthenticationType("Application");
I assume that it has to be in AuthenticationMode = Active, else the Authorize attributes wont work?
What exactly needs to be in my webapi controller to do the exchange for a cookie?
public async Task<HttpResponseMessage> Get()
{
var context = Request.GetOwinContext();
//Validate Username and password
context.Authentication.SignIn(new AuthenticationProperties()
{
IsPersistent = true
},
new ClaimsIdentity(new[] { new Claim(ClaimsIdentity.DefaultNameClaimType, "MyUserName") }, "Application"));
return Request.CreateResponse(HttpStatusCode.OK);
}
Is above okay?
Current Solution
I have added my BasicAuthenticationMiddleware as the active one, added the above CookieMiddleware as passive.
Then in the AuthenticateCoreAsync i do a check if I can login with the Cookie,
var authContext = await Context.Authentication.AuthenticateAsync("Application");
if (authContext != null)
return new AuthenticationTicket(authContext.Identity, authContext.Properties);
So I can now exchange from webapi controller a username/pass to a cookie and i can also use the Basic Scheme directly for a setup that dont use cookies.
If web api and javascript file are from different origins and you have to add authorization header or cookie header to the request, you cannot prevent browser from sending preflight request. Otherwise it will cause CSRF attack to any protected web api.
You can use OWIN Cors package or Web API Cors package to enable CORS scenario, which can handle the preflight request for you.
OWIN cookie middleware is responsible for setting auth cookie and verify it. It seems to be what you want.
BTW, Basic auth challenge can cause browser to pop up browser auth dialog, which is not expected in most of the web application. Not sure if it's what you want. Instead, using form post to send user name and password and exchange them with cookie is what common web app does.
If you have VS 2013 RC or VWD 2013 RC installed on your machine, you can create an MVC project with Individual auth enabled. The template uses cookie middleware and form post login. Although it's MVC controller, you can simply convert the code to Web API.
[Update]
Regarding preflight request, it will be sent even with cookie header according to the spec. You may consider to add Max Age header to make it be cached on the browser.
JSONP is another option which doesn't require preflight.
[Update2] In order to set cookie by owin middleware, please use the following sample code.
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.ApplicationAuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, "Test"));
AuthenticationManager.AuthenticationResponseGrant = new AuthenticationResponseGrant(identity, new AuthenticationProperties()
{
IsPersistent = true
});