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
Related
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");
}
}
}
Since I am new to webapi my project fornt-end is react js and backend is Asp.net core 3.1 webapi
so i am redirecting to page given below with query string format username|redirecturl
username:Identify user
redirecturl:controller url where i will receive posted form data
--Request Url----
https://example.com/anonymouspage.aspx?req=username|https://localhost:5001/api/ControllerName/PostTodoItem
above page will redirect back to controller url which i have share in query string with following given response
---Response---
<form name='ecom' id='test' action='Returm URL' method='post'>
<input type='hidden' name='txtUserName' value='Process Completed Successfully'/>
<input type='hidden' name='givenUserName' value='abc'/>
</form>
---Controller----
[AllowAnonymous]
[HttpPost]
public async Task<string> PostTodoItem()
{
NameValueCollection nvc = Request.Form;
userName = nvc["givenUserName"];
status = nvc["txtUserName"];
}
but i am getting 405 http error i need help on this
i am redirecting to page given below with query string format username|redirecturl
https://example.com/anonymouspage.aspx?req=username|https://localhost:5001/api/ControllerName/PostTodoItem
above page will redirect back to controller url
If you do redirection via HttpResponse.Redirect Method in your .aspx code behind, it would make a GET request to your API action, but you applied [HttpPost] attribute to your API action, which cause the issue.
If you'd like to make a POST request to you API action PostTodoItem, you can refer to the following code snippet.
using (HttpClient httpClient = new HttpClient())
{
// in your code logic
// you may need to generate following contnet
// based on the retrieved from data
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("givenUserName", "abc"),
new KeyValuePair<string, string>("txtUserName", "Process Completed Successfully")
});
var result = httpClient.PostAsync("https://localhost:5001/api/ControllerName/PostTodoItem", content).Result;
//code logic here
//...
Test Result
I'm trying to use the framework's tools to add some simple CSRF validation to an ASP.NET Core React SPA. The application itself is essentially a create-react-app setup (a single index.html with a root element and everything else is loaded in from bundled JavaScript).
Tinkering with some information found on links such as this one, I've set the following in my Startup.ConfigureServices:
services.AddAntiforgery(options => options.Cookie.Name = "X-CSRF-TOKEN");
And confirmed in my Chrome tools that the cookie is being set. If I omit the above line, a cookie is still set with a partially randomized name, such as: .AspNetCore.Antiforgery.RAtR0X9F8_w Either way the cookie is being set. I've also confirmed that any time I re-start the whole application the cookie value is updated, so the framework is actively setting this cookie.
Observing network requests in my Chrome tools, I confirm that the cookie is being sent to the server on AJAX request. Placing a breakpoint on the server and observing the Request.Cookies value in a controller action also confirms this.
However, if I decorate any such AJAX requested action with [ValidateAntiForgeryToken] then the response is always an empty 400.
Is there a configuration step I've missed somewhere? Perhaps the action attribute is looking in the wrong place and I need to use a different validation?
I just inspect the log and find out there's an exception:
Microsoft.AspNetCore.Antiforgery.AntiforgeryValidationException: The required antiforgery cookie ".AspNetCore.Antiforgery.HPE6W9qucDc" is not present.
at Microsoft.AspNetCore.Antiforgery.Internal.DefaultAntiforgery.ValidateRequestAsync(HttpContext httpContext)
at Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.ValidateAntiforgeryTokenAuthorizationFilter.OnAuthorizationAsync(AuthorizationFilterContext context)
It indicates that you forgot to configure the cookie name :
public void ConfigureServices(IServiceCollection services)
{
//services.AddAntiforgery();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
// In production, the React files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
}
So I just add a configuration as below :
public void ConfigureServices(IServiceCollection services)
{
services.AddAntiforgery(o => {
o.Cookie.Name = "X-CSRF-TOKEN";
});
// ...
}
and it works now.
Also, if you would like to omit the line of services.AddAntiforgery(options => options.Cookie.Name = "X-CSRF-TOKEN"); , you can use the built-in antiforgery.GetAndStoreTokens(context) method to send cookie:
app.Use(next => context =>
{
if (context.Request.Path == "/")
{
//var tokens = antiforgery.GetTokens(context);
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("X-CSRF-TOKEN", tokens.CookieToken, new CookieOptions { HttpOnly = false });
context.Response.Cookies.Append("X-CSRF-FORM-TOKEN", tokens.RequestToken, new CookieOptions { HttpOnly = false });
}
return next(context);
})
Both should work as expected.
The accepted answer here is extremely incorrect when it suggests to send both cookies via JS-readable cookies:
// do not do this
context.Response.Cookies.Append("X-CSRF-TOKEN", tokens.CookieToken, new CookieOptions { HttpOnly = false });
context.Response.Cookies.Append("X-CSRF-FORM-TOKEN", tokens.RequestToken, new CookieOptions { HttpOnly = false });
If you send both the Cookie token and the Request token in a Cookie that is readable by JS, you are defeating the purpose of having a Cookie token and a Request token.
The purpose of using both tokens is to make sure that
you have a valid session (the HTTP-only Cookie proves this),
you have requested a form from the site using this valid session (the HTTP-readable Cookie or another method can prove this), and
you are submitting the form from the same valid session
Why It's Wrong.
The Request Token
The Request Token ensures that you have actually loaded a page (example.com/example-page). Think about this: if you are logged in to example.com as an administrator, a request from anywhere from your browser (where CORS allows the necessary properties) can successfully validate against Cookie-based CSRF Validation and your authentication.
However, by adding the Request Token, you are confirming that your browser also actually loaded a request to the form (or at least, the site) before submitting it. This is usually done with a hidden input. This is automatically done by using the Form Tag Helper in Asp.Net.
<form action="/myEndpoint" method="POST">
<input name="__RequestVerificationToken" type="hidden" value="#antiforgery.GetAndStoreTokens(context).RequestToken" />
<button type="submit">Submit</button>
</form>
It can also be set .. anywhere. like window.CSRFRequestToken, and manually added to a POST request, like in this fetch example:
fetch('/myEndpoint', { method: 'POST', headers: { 'X-XSRF-Token': window.myCSRFRequestToken, 'Bearer': window.mySuperSecretBearerToken } };
The Cookie Token
In the above contrived example, the user is logged in via a bearer token via OAuth or something (not recommended, use HTTP-only Cookies in a browser environment).
The Cookie Token ensures that a malicious script cannot exfiltrate your Request Token and send requests on your behalf. Without it, in a supply chain attack, a malicious user can send your secrets to a malicious actor:
window.addEventListener('load', => sendMySuperSecretInfoToTheShadowRealm(window.CSRFRequestToken, window.mySuperSecretBearerToken));
Now the malicious user could send a request from wherever they want using your CSRF and bearer token to authenticate. BUT! Not if you have your good friend HTTP-only Cookie-based CSRF Validation -- because JavaScript cannot read HTTP-only cookies.
The Solution
Asp.Net combines these solutions by setting both a Cookie Token and a Request Token. Therefore, when you are sending a request to AspNet you send both:
The cookie:
Cookies.Append('X-CSRF-Token', #antiforgery.GetAndStoreTokens(context).CookieToken);
and either the aspnet form helper tag:
<form action="myEndpoint" />
or manually print the token:
<form action="myEndpoint" asp-antiforgery="false">
#Html.AntiForgeryToken()
</form>
or provide the token manually to your scripts:
window.myCSRFRequestToken = "#antiforgery.GetAndStoreTokens(context).RequestToken)";
fetch('/myEndpoint', { method: 'POST', headers: { 'X-CSRF-Token': window.myCSRFRequestToken };
Don't take my word for it
Please please read this page fully in case I didn't explain anything clearly:
https://learn.microsoft.com/en-us/aspnet/core/security/anti-request-forgery?view=aspnetcore-6.0
A final note:
In the documentation above, the very last example uses a cookie to send the request cookie. This is very different in a subtle way than the answer here. The accepted answer sends both cookies as Javascript-readable { HttpOnly = false }. This means JavaScript can read both and a malicious user can read both and craft a special request themselves that will validate against both Cookie and Request CSRF validations (where CORS allows).
In the documentation, one is sent via an HTTP only cookie (this cannot be read by JS, only used for Cookie-based CSRF validation) and the other is sent via an HTTP-readable cookie. This HTTP-readable cookie MUST be read by JavaScript and used with one of the above methods (form input, header) in order to validate CSRF Request Token Validation.
I have a static website composed of html/css/javascript files. The website is automatically generated and updated frequently.
Instead of authorizing access to the website with a username/password (basic auth), I would like to have users authenticate using Google Sign-in/openID Connect, and then control access via a whitelist of gmail addresses.
What is the simplest way to set this up?
I ended up using oauth2_proxy which is exactly what I was looking for.
I configured to do the following:
oauth2_proxy listens on 0.0.0.0:443
When a user connects, the Google sign-in flow is initiated
After sign-in, it validates the user's email address against a whitelist
After successful validation, oauth2_proxy proxies the request to an upstream nginx server listening on 127.0.0.1:8080
Another way to add authentication or gated content to any static site:
1) First load a static container page (header, footer) and implement user Authentication js code using Auth0, firebase, okta etc.
2) When user successfully logs in then make an ajax api call passing that auth access_token to retrieve the sensitive content.
3) Load/append that sensitive content in the site using js.
Of Course, there has to be one server/serverless function which would listen to that ajax api call, authenticate it and sends the content back to the browser.
This is called client side authentication.
More on this: https://auth0.com/blog/ultimate-guide-nextjs-authentication-auth0/
Best way would be to use Firebase Auth!
Check it out at https://firebase.google.com/docs/auth/
You could check if the user is authenticated or not in this way.
<script type="text/javascript">
function initApp() {
// Listening for auth state changes.
// [START authstatelistener]
firebase.auth().onAuthStateChanged(function (user) {
if (user) {
//User is signed in.
if (!emailVerified) {
//Additional check for email verification
}
} else {
// User is signed out.
}
});
// [END authstatelistener]
}
window.onload = function () {
initApp();
};
</script>
Check out https://authorizer.dev/. It's free and open source.
This tutorial explains how to get started for static sites (follow the UMD section): https://docs.authorizer.dev/authorizer-js
<script src="https://unpkg.com/#authorizerdev/authorizer-js/lib/authorizer.min.js"></script>
<script type="text/javascript">
const authorizerRef = new authorizerdev.Authorizer({
authorizerURL: `YOUR_AUTHORIZER_INSTANCE_URL`,
redirectURL: window.location.origin,
clientID: 'YOUR_CLIENT_ID', // obtain your client id from authorizer dashboard
});
// use the button selector as per your application
const logoutBtn = document.getElementById('logout');
logoutBtn.addEventListener('click', async function () {
await authorizerRef.logout();
window.location.href = '/';
});
async function onLoad() {
const res = await authorizerRef.authorize({
response_type: 'code',
use_refresh_token: false,
});
if (res && res.access_token) {
// you can use user information here, eg:
const user = await authorizerRef.getProfile({
Authorization: `Bearer ${res.access_token}`,
});
const userSection = document.getElementById('user');
const logoutSection = document.getElementById('logout-section');
logoutSection.classList.toggle('hide');
userSection.innerHTML = `Welcome, ${user.email}`;
}
}
onLoad();
</script>
Here is a video demo: https://youtu.be/uQka5O2RwpU?t=97
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.