Blazor Server auth with jwt - asp.net-core

I have been trying for a few days to do client-side persistence token authorization. The problem is that we essentially cannot use IJSRunrime when a user request comes in to check his authentication, because ServerRendering does not run before initialization on the client. I also tried to use cookies in order to save them in the request, but we cannot do this either due to the fact that the request has already been started, for example, in the same MVC, we can do this without problems.
Detailed errors:
IJSRunrime - "JavaScript interop calls cannot be issued at this time"
Cookie - "headers are read-only, response has already started"
I create this code for example and my question - How save token on the client side?
UPD: I don't want to reload SPA application, some examples do it and I can't use HttpContext.SignIn because have Cookie error.
#page "/SignIn"
<EditForm EditContext="#editContext" OnValidSubmit="SendRequestSubmit" class="auth__form">
<DataAnnotationsValidator />
<div class="auth__item">
<h4 class="auth__actions-title actions-title">Email<span>*</span></h4>
<InputText #bind-Value="signInRequest.Email" class="input-actions" />
</div>
<div class="auth__item">
<h4 class="auth__actions-title actions-title">Password<span>*</span></h4>
<InputText #bind-Value="signInRequest.Password" type="password" class="input-actions" />
</div>
<button class="auth__confirm actions-btn btn" disabled="#sendRequestLoading">
Sign In
</button>
</EditForm>
#code {
// it is code for example
private async void SendRequestSubmit()
{
var auth = accountSerrvice.auth(Email, Password);
if(auth == true && auth.Token != null)
{
// Save auth.Token in the client side
}
else
{
throw new Exception("Test error");
}
}
}

Related

.NET CORE and ADFS no Claims Available

Migrating to .NET Core 3 from a 4.6 project and I'm not 100% sure I am implementing things properly.
I followed the steps in this article, making sure to configure startup.cs following the code sample under the "Use WS-Federation without ASP.NET Core Identity" section.
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/ws-federation?view=aspnetcore-3.0
Login seems to be working in that I'm redirected to MS login and sent back to my app with the expected cookies, for example MSISAuthenticated. But, user.identity.isauthenticated is always false and I have no claims available. Is this expected behavior? Perhaps I'm not configuring things properly? Ideally I'd like to be able to check if a user is authenticated and access the claims.
I've come across a number of articles about adding policies based on groups, but how would [Authorize (Policy="SomeGroup")] even work if no claims are available?
ConfigureServices Code:
enter image description here
Configure Code:
enter image description here
Controller Action:
public IActionResult Index()
{
var identity = (ClaimsIdentity)User.Identity;
ViewBag.Claims = identity.Claims;
return View();
}
View Code:
#using System.Security.Claims;
#{
ViewBag.Title = "Home Page";
IEnumerable<Claim> claims = (IEnumerable<Claim>)ViewBag.Claims;
}
#if (User.Identity.IsAuthenticated)
{
<div class="jumbotron">
<h1>Successful Sign On!</h1>
</div>
<div class="row">
<div class="col-md-12">
<h2>WS Federation Services Claims</h2>
#foreach (Claim claim in claims)
{
<p>
<b>#(claim.Type.ToString())</b>
<br />
#(claim.Value.ToString()) (type: #(claim.ValueType.ToString()))
<hr />
</p>
}
</div>
</div>
}
else
{
<div class="jumbotron">
<h1>SSO Test</h1>
<p class="lead">To sign in using Microsoft's single sign-on service, click the button below.</p>
<p>Sign in ยป</p>
</div>
}
perhaps the fact is that you are not send the desired ResourceUrl to ADFS. Then ADFS considers the default resource and issues a token without claims.
See more info on 3 step in "High level AD FS authentication flow"
enter link description here
AD FS identifies the resource which the client wants to access through
the resource parameter passed in the auth request. If using MSAL
client library, then resource parameter is not sent. Instead the
resource url is sent as a part of the scope parameter: scope =
[resource url]//[scope values e.g., openid].

ASP.NET Core - use external provider but *keep* Default Identity database

I want the database that comes with the Default Identity provider in ASP.NET Core. However, I'd like users to login exclusively with their Microsoft account.
So at the moment, I have this in my user LoginDisplay.razor file:
<AuthorizeView>
<Authorized>
Hello, #context.User.Identity.Name!
Log out
</Authorized>
<NotAuthorized>
Register
Log in
</NotAuthorized>
</AuthorizeView>
When the user clicks "Log in" they're taken to the regular login form:
Here they can click on the "Microsoft Account" button. What I would like to do is skip the default login screen and go directly to the Microsoft Account workflow.
How would I do that?
Keeping the identity database offers me a couple of benefits:
I plan to add more data to the database - so it's handy if I can refer to accounts that exist in the same database
It's possible that I may need to give users access to the site that do not have a Microsoft account
Update
Based on feedback, I've implemented the following:
#inject Data.Services.AntiForgery antiforgery;
<form id="external-account" method="post" class="inline-block form-horizontal" action="/Identity/Account/ExternalLogin?returnUrl=%2F">
<button type="submit" name="provider" value="microsoft" title="Log in using your Microsoft Account account">Login</button>
<input name="__RequestVerificationToken" type="hidden" value="#antiforgery.Generate()">
</form>
And here's my utility class that I used to work around the anti-forgery request token (in Blazor):
public class AntiForgery
{
public IAntiforgery Antiforgery { get; private set; }
public IHttpContextAccessor Accessor { get; private set; }
public AntiForgery( IAntiforgery antiforgery, IHttpContextAccessor accessor )
{
Antiforgery = antiforgery;
Accessor = accessor;
}
public string Generate()
{
// Code stolen from:
// * https://stackoverflow.com/questions/45254196/asp-net-core-mvc-anti-forgery; and
// * https://stackoverflow.com/questions/53817373/how-do-i-access-httpcontext-in-server-side-blazor
return Antiforgery.GetAndStoreTokens( Accessor.HttpContext ).RequestToken;
}
}
For the utility class to work, the following was added to my Startup file:
services.AddSingleton<AntiForgery>();
services.AddHttpContextAccessor();
Well, you can simply just hide the login form itself, showing only the Microsoft Account button. However, it is not possible to send the user directly into that flow. It requires an initial post, which is going to require action on the user's part, i.e. clicking the button.
For what it's worth. If you have a "Login" type link, you can code this in the same way as the Microsoft Account button, so that it then initiates the flow when the user clicks "Login". However, you'll still need an actual login page to redirect to for authorization failures, and that would still require an explicit button press there.
You could directly pass the provider name Microsoft to your external login function using asp-route-provider.
For asp.net core 2.2+, Identity is scaffolded into identity area with Razor Pages.
1.Login link.
<a asp-area="Identity" asp-page="/Account/ExternalLogin" asp-page-handler="TestExternal" asp-route-provider="Microsoft">Log in</a>
2.ExternalLogin.cshtml.cs
public IActionResult OnGetTestExternalAsync(string provider, string returnUrl = null)
{
var redirectUrl = Url.Page("./ExternalLogin", pageHandler: "Callback", values: new { returnUrl });
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
return new ChallengeResult(provider, properties);
}
3.Startup.cs
services.AddAuthentication().AddMicrosoftAccount(microsoftOptions =>
{
//use your own Id and secret
microsoftOptions.ClientId = Configuration["Authentication:Microsoft:ClientId"];
microsoftOptions.ClientSecret = Configuration["Authentication:Microsoft:ClientSecret"];
});

IdentityServer4 Host with SPA front end

I'm trying to use identity server 4 as a SSO site with a front-end written as an SPA (framework not important)
The Example project uses MVC, which when the user logs in the page posts to a controller that redirects the browser to the return URL.
I'm having trouble modifying this flow to work in a more AJAX fashion. Firstly I want to be able to submit the username/password to an API controller so that I can get back validation errors etc without doing a page refresh. Given a successful login I then need to redirect the browser to the returnUrl, but I cannot get this to work and the callback url returns the user back to the login page again rather than redirecting to the client app logged in.
This is what my login endpoint looks like:
[HttpPost]
[Route("api/identity/login")]
public async Task<IActionResult> Login(LoginInputModel model)
{
// check credentials in model etc
await _eventsService.RaiseAsync(new UserLoginSuccessEvent(model.Email, subjectId, model.Email));
await HttpContext.SignInAsync(subjectId, model.Email, new AuthenticationProperties());
return Ok();
}
And simple form as a front-end, this is hosted on a static html page:
<form>
<label for="email">Email</label>
<input id="email" type="email" />
<label for="password">Password</label>
<input id="password" type="password" />
<button onclick="login()" type="submit">Log me in</button>
</form>
<script>
var email = document.querySelector('#email').value;
var password = document.querySelector('#password').value;
var returnUrl = unescape(window.location.search.replace('?returnUrl=', ''));
fetch('/api/identity/login', {
body: JSON.stringify({ email, password }),
headers: new Headers({
'Content-Type': 'application/json'
}),
method: 'POST'
}).then(() => {
var returnUrl = unescape(window.location.search.replace('?returnUrl=', ''));
window.location = window.location.origin + returnUrl;
})
</script>
On a 200 response I use javascript to redirect the browser to the returnUrl.
I'm not sure what I'm missing to get this to work. Do I need to sign the user in and redirect all in one call?
I am modifying an existing example app here which does work with the direct post/redirect method as expected, so both host and client config is unchanged: https://github.com/BenjaminAbt/Samples.AspNetCore-IdentityServer4
After investigating some logs and watching the requests a little more carefully I realised that the login response from the AJAX request was not setting the authentication cookie on the browser.
Setting the credentials option in the fetch request to 'same-origin' fixed the issue
thanks to this stackoverflow answer: https://stackoverflow.com/a/39233628

How to use Basic authentication for web browser requests with ServiceStack?

I have a REST API built using ServiceStack. I am using BasicAuthentication without any issues when calling the REST APIs (I am registering the AuthFeature with the BasicAuthProvider).
Now I am trying to build some HTML management pages. These should also be authenticated.
The [Authenticate] attribute redirects to the /login page, so I created the following DTO and matching service to handle logins:
[DefaultView("Login")]
public class SiteLoginService : EnshareServiceBase
{
public object Get(SiteLoginRequest req)
{
return new SiteLoginRequest();
}
public object Post(SiteLoginRequest req)
{
//I am trying to use the registered IAuthProvider, which is the BasicAuthProvider
var authProvider = ResolveService<IAuthProvider>();
authProvider.Authenticate(this, EnshareSession,
new ServiceStack.ServiceInterface.Auth.Auth()
{
Password = req.Password,
UserName = req.UserName
});
return HttpResult.Redirect(req.Redirect);
}
}
[Route("/login")]
public class SiteLoginRequest : IReturn<SiteLoginRequest>
{
public string Redirect { get; set; }
public string Password { get; set; }
public string UserName { get; set; }
}
However, the BasicAuthProvider always throws HttpError: "Invalid BasicAuth credentials" when I fill in username and password on the Login view page and POST these to the SiteLoginService. It is probably because the web browser is not filling in the Basic auth header, but I do not know how to authenticate with filled in username and password.
How to properly authenticate site users against the AuthProvider which works with the existing REST API?
If you are passing the Username & Password as a post, then as you suspect you are not doing Basic Authentication.
This article explains how to do basic authentication with JavaScript. From the article:
function login() {
var username = document.getElementById(this.id + "-username").value;
var password = document.getElementById(this.id + "-password").value;
this.http.open("get", this.action, false, username, password);
this.http.send("");
if (http.status == 200) {
document.location = this.action;
} else {
alert("Incorrect username and/or password.");
}
return false;
}
ServiceStack also supports other forms of authentication including sending a username and password via a POST if that is what you want. If you explain your requirements we can give some recommendations.
I figured I need to include also the CredentialsAuthProvider in the AuthFeature, which will expose /auth/credentials service which I form post a form to.
//this inherits the BasicAuthProvider and is used to authenticate the REST API calls
var myCustomAuthProvider = new CustomAuthProvider(appSettings);
var credentialsProvider = new CredentialsAuthProvider(appSettings);
container.Register<IAuthProvider>(myCustomAuthProvider);
container.Register<CredentialsAuthProvider>(credentialsProvider);
var authFeature = new AuthFeature(() => new EnshareSession(new MongoTenantRepository()),
new IAuthProvider[] {
myCustomAuthProvider,
credentialsProvider
})
So I specified the action in my login form as /auth/credentials, while providing the required UserName and Password fields.
<form action="/auth/credentials" method="post">
<p class="entryfield">
#Html.LabelFor(m => m.UserName, "Login name:")
#Html.TextBoxFor(u => u.UserName)
</p>
<p class="entryfield">
#Html.LabelFor(m => m.Password)
#Html.PasswordFor(m => m.Password)
</p>
<input class="formbutton" type="submit" value="Login" />
</form>
When the form is posted, it hits the authentication code flows properly (TryAuthenticate is called in my IUserAuthRepository and returns true).
Ultimately the request receives a 302 and my login form at /login is redisplayed.
HTTP/1.1 302 Found
Server: ASP.NET Development Server/10.0.0.0
Date: Wed, 30 Oct 2013 08:15:54 GMT
X-AspNet-Version: 4.0.30319
X-Powered-By: ServiceStack/3,969 Win32NT/.NET
Location: http://localhost:64944/login?redirect=%2fadmin
Set-Cookie: X-UAId=3; expires=Sun, 30-Oct-2033 08:15:54 GMT; path=/; HttpOnly
It is setting the session cookie (X-AUId) and the user is properly authenticated. Subsequent web browser requests to Services decorated with the Authenticate attribute succeed.
So the only missing part is how to ensure that the user is properly redirected after posting to /auth/credentials.
To ensure the redirection works, a quick look at the has shown that a Continue parameter is expected.
So this is how the login form needs to look like (I reused the Auth class from ServiceStack for the model):
#inherits ViewPage<ServiceStack.ServiceInterface.Auth.Auth>
#{
Layout = "AdminLayout";
}
<form action="/auth/credentials" method="post">
<p class="entryfield">
#Html.LabelFor(m => m.UserName, "Login name:")
#Html.TextBoxFor(u => u.UserName)
</p>
<p class="entryfield">
#Html.LabelFor(m => m.Password)
#Html.PasswordFor(m => m.Password)
</p>
#Html.HiddenFor(m => m.Continue)
<input type="submit" value="Login" />
</form>
The Continue property is populated in the service from the Redirect property of its model.

Upload a file MVC 4 Web API .NET 4

I'm using the version of MVC that shipped with Visual Studio 2012 express. (Microsoft.AspNet.Mvc.4.0.20710.0)
I assume this is RTM version.
I've found plenty of examples online which all use this code:
public Task<HttpResponseMessage> PostFormData()
{
// Check if the request contains multipart/form-data.
if (!Request.Content.IsMimeMultipartContent())
{
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
}
string root = HttpContext.Current.Server.MapPath("~/App_Data");
var provider = new MultipartFormDataStreamProvider(root);
// Read the form data and return an async task.
var task = Request.Content.ReadAsMultipartAsync(provider).
ContinueWith<HttpResponseMessage>(t =>
{
if (t.IsFaulted || t.IsCanceled)
{
return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, t.Exception);
}
// This illustrates how to get the file names.
foreach (MultipartFileData file in provider.FileData)
{
Trace.WriteLine(file.Headers.ContentDisposition.FileName);
Trace.WriteLine("Server file path: " + file.LocalFileName);
}
return Request.CreateResponse(HttpStatusCode.OK);
});
return task;
}
But this code always ends up in continueWith where t.IsFaulted == true. The exception reads:
Unexpected end of MIME multipart stream. MIME multipart message is not
complete.
Here is my client form. NOthing fancy, I want to do jquery form pluging for ajax upload, but I can't even get this way to work.
<form name="uploadForm" method="post" enctype="multipart/form-data" action="api/upload" >
<input type="file" />
<input type="submit" value="Upload" />
</form>
I've read that it is caused by the parser expecting /CR /LF at the end of each message, and that bug has been fixed in June.
What I cannot figure out is, if it was really fixed, why isn't it included this version of MVC 4? Why do so many examples on the internet tout that this code works when it does not in this version of MVC 4?
You are missing a name attribute on your file input.
<form name="uploadForm" method="post" enctype="multipart/form-data" action="api/upload" >
<input name="myFile" type="file" />
<input type="submit" value="Upload" />
</form>
Inputs without it will not get submitted by the browser. So your formdata is empty resulting in IsFaulted being asserted.