I'm busting my head against the wall here. Please help.
I've developed an MVC 4.0 web application.
I am using the Facebook SDK to log in to the website.
For argument sake, I have 2 action methods: Index and Info.
Index is where the user clicks the "Log in using Facebook" button, and after he enters his credentials, he is supposed to be redirected to Info action method. (from there he can log out).
This is the Index method:
[AllowAnonymous]
public ActionResult Index()
{
return View();
}
I have this in my web.config:
<authentication mode="Forms">
<forms loginUrl="~/Home/Index" timeout="2880" />
</authentication>
These are the settings I entered at Facebook developers for my app:
Canvas URL:
http://localhost/[AppName]/
Secure Canvas URL:
https://localhost/[AppName]/
Site URL:
http://localhost/[AppName]/
Valid OAuth redirect URIs:
http://localhost/
and also
http://localhost/[AppName]/home/index
The problem is:
I want to make the Info action method only available to authorized users, so naturally, I decorated it with [Authorized] attribute:
[Authorize]
public ActionResult Info()
{
var client = new FacebookClient();
var oauthResult = client.ParseOAuthCallbackUrl(Request.Url);
// Build the Return URI form the Request Url
var redirectUri = new UriBuilder(Request.Url);
redirectUri.Path = Url.Action("Info", "Home");
// Exchange the code for an access token
dynamic result = client.Get("/oauth/access_token",
new { client_id = AppId,
redirect_uri = redirectUri.Uri.AbsoluteUri,
client_secret = SecId,
code = oauthResult.Code });
// Read the auth values
string accessToken = result.access_token;
Session["access_token"] = accessToken;
DateTime expires = DateTime.UtcNow.AddSeconds(Convert.ToDouble(result.expires));
// Get the user's profile information
dynamic me = client.Get("/me",
new
{
fields = "first_name,last_name,email,picture",
access_token = accessToken
});
var userInfo = new InfoModel()
{
firstName = me.first_name,
lastName = me.last_name,
emailId = me.email,
picture = me.picture,
accessToken = result.access_token
};
return View("Info", userInfo);
}
So now I have two unwated scenraios: I can either log in to my app, BUT after log in, i'm redirected to Home/Index, instead to Home/Info.
OR
I can log in properly (I am redirected to Home/Info after log in, as expected) BUT I can also browse directly to Info action method (since I had to remove the [Authorize] attribute in order to log in.
It's like a Rubik's Cube!
I'm sorry the post is long, but I had to elaborate.
I found and answer.
It's involve a little bit of a work-around, but it's as good:
[AllowAnonymous]
public ActionResult Info()
{
if (Request.Url.AbsoluteUri.Contains("Home/Info?code"))
{
var client = new FacebookClient();
var oauthResult = client.ParseOAuthCallbackUrl(Request.Url);
// Build the Return URI form the Request Url
var redirectUri = new UriBuilder(Request.Url);
redirectUri.Path = Url.Action("Info", "Home");
// Exchange the code for an access token
dynamic result = client.Get("/oauth/access_token",
new
{
client_id = AppId,
redirect_uri = redirectUri.Uri.AbsoluteUri,
client_secret = SecId,
code = oauthResult.Code
});
// Read the auth values
string accessToken = result.access_token;
Session["access_token"] = accessToken;
DateTime expires = DateTime.UtcNow.AddSeconds(Convert.ToDouble(result.expires));
// Get the user's profile information
dynamic me = client.Get("/me",
new
{
fields = "first_name,last_name,email,picture",
access_token = accessToken
});
var userInfo = new InfoModel()
{
firstName = me.first_name,
lastName = me.last_name,
emailId = me.email,
picture = me.picture,
accessToken = result.access_token
};
FormsAuthentication.SetAuthCookie(userInfo.emailId, true);
return View("Info", userInfo);
}
else
return View("Index");
}
As you can see, I had a condition at the beginning of the Action method:
If, and only if the url contains the word "code" then the user can authenticate, otherwise, he is redirected back to the first page.
If the word "code" exists, it means that the user has a token and therefore he has already entered his credentials correctly, so now his details can be extracted.
At the end of the extraction process, just before the redirection to the view, I added:
FormsAuthentication.SetAuthCookie(userInfo.emailId, true);
In order to save the cookie. This is what required for the [Authorize] attribute to function correctly, otherwise the code will act as though you are not authenticated, even though you are.
Now I can safely add an [Authorize] attribute to all the action methods I wish to make available to authenticated users only.
Don't forget to add [AllowAnonymous] attribute to every action method that are used as part of the authentication process, otherwise the user won't be able to browse their respective views.
BTW,
If the user tries to type he will get an error, since he still needs the token.
Related
I'm trying to create an external login scheme for facebook, google and linkedin without using identity framework. I have an api that stores all users and do some authentication stuffs. Right now I'm kind of lost on how to get the information from the external login.
I'm issuing a challenge like this.
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult ExternalLogin(string provider)
{
//Issue a challenge to external login middleware to trigger sign in process
return new ChallengeResult(provider);
}
This works well, it redirects me to either google, facebook or linkedinn authentication.
Now on this part:
public async Task<IActionResult> ExternalLoginCallback()
{
//Extract info from externa; login
return Redirect("/");
}
All I want is to get the information that was provided by the external login.
I have tried what I found from my research,
var result = await HttpContext.AuthenticateAsync(provider);
if (result?.Succeeded != true)
{
return Redirect("/");
}
var externalUser = result.Principal;
var claims = externalUser.Claims.ToList();
First of all I I'm not sure if a simple ?provider=Google on my callback string will pass the provider name I specify so it can be used to check the sign in scheme. I guess this is incorrect. Secondly, I tried hard coding await HttpContext.AuthenticateAsync("Google") and when it reach this code, the debug stops. I'm not sure why.
I've seen the generated code when creating a project with single authentication.
var info = await _signInManager.GetExternalLoginInfoAsync();
Sadly, I'm won't be able to use identity since I don't have a user store and my application will be consuming an API.
First you need to create a custom cookie handler. I myself had problems with:
No IAuthenticationSignInHandler is configured to handle sign in for
the scheme: Bearer
I had to add a cookie handler that will temporarily store the outcome of the external authentication, e.g. the claims that got sent by the external provider. This is necessary, since there are typically a couple of redirects involved until you are done with the external authentication process.
Startup
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(o =>
{
o.TokenValidationParameters = tokenValidationParameters;
})
.AddCookie("YourCustomScheme")
.AddGoogle(googleOptions =>
{
googleOptions.SignInScheme = "YourCustomScheme";
googleOptions.ClientId = "x";//Configuration["Authentication:Google:ClientId"];
googleOptions.ClientSecret = "x";//Configuration["Authentication:Google:ClientSecret"];
//googleOptions.CallbackPath = "/api/authentication/externalauthentication/signin-google";
});
The important part here is "YourCustomScheme".
Now it's time to retrieve the user information from the claims provided by the external authentication in the callback action.
Controller
[AllowAnonymous]
[HttpPost(nameof(ExternalLogin))]
public IActionResult ExternalLogin(ExternalLoginModel model)
{
if (model == null || !ModelState.IsValid)
{
return null;
}
var properties = new AuthenticationProperties { RedirectUri = _authenticationAppSettings.External.RedirectUri };
return Challenge(properties, model.Provider);
}
[AllowAnonymous]
[HttpGet(nameof(ExternalLoginCallback))]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
//Here we can retrieve the claims
var result = await HttpContext.AuthenticateAsync("YourCustomScheme");
return null;
}
VoilĂ ! We now have some user information to work with!
Helpful link
http://docs.identityserver.io/en/release/topics/signin_external_providers.html
I too had this issue and see if the below code works for you.
I wanted to extract the full name after Google/FB authentication.
var info = await _signInManager.GetExternalLoginInfoAsync();
TempData["fullname"] = info.Principal.FindFirstValue(ClaimTypes.Name);
Can anyone give me solution for this issue (or article, keyword ):
One user create a product, and back-end will be send mail for admin to approve the product, this mail has contained the link:
http://localhost:11260/#/productHandle/116796
I want admin don't need login in system, and can access this link.
because the current code in Global.asax check cookies:
protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
{
HttpCookie ck = Request.Cookies[FormsAuthentication.FormsCookieName];
if (ck != null && !String.IsNullOrEmpty(ck.Value))
{
FormsAuthenticationTicket fat = FormsAuthentication.Decrypt(ck.Value);
UserProfile profile = JsonConvert.DeserializeObject<UserProfile>(fat.UserData);
MyPrincipal myPrincipal = new MyPrincipal(profile.Username);
myPrincipal.UsrProfile = profile;
HttpContext.Current.User = myPrincipal;
}
}
---after access successfully, i intended to encrypt this link. but step above don't work..
thank for help me!
We have a similar application that involves a user requesting access to our document imaging system, and then an email is sent to our application admins for approval. The general idea looks like this:
[HttpPost]
public ActionResult RegisterNewUser(NewUser model)
{
if (ModelState.IsValid)
{
var pwSalt = repository.CreateSalt();
var newUser = User();
newUser.Username = model.Username;
newUser.Email = model.Email;
newUser.PasswordSalt = pwSalt;
newUser.Password = repository.CreatePasswordHash(model.Password, pwSalt);
newUser.IsApproved = false;
newUser.RegisterDate = DateTime.Now;
db.Users.Add(newUser);
db.SubmitChanges();
ConfirmationEmail(model.Username);
return RedirectToAction("RegistrationSuccess");
}
}
The above code is the post-action for a new user who's just registered for our application. It adds the user to the db table. The model contains fields like name, username, email, etc (I also have a function "CreatePasswordHash" which uses the user's password and a generated SALT to created an encrypted password). One thing to note is that the Users table contains a column "IsApproved", which sets to "false" by default. Without this value changing to "true", the user won't be able to use the application. Within this action is another function named "ConfirmationEmail" which is where we send an email to one of the admins for approval.
public void ConfirmationEmail(string username)
{
var user = db.Users.Single(u => u.Username == username);
string mailBody = "A new user requires approval for document imaging.\n\n"
+ "Name: " + user.Name + "\n\n"
+ "\n\n"
+ "If you approve, follow the link below: \n\n"
+ "http://example.com/Imaging/Account/Approval/" + user.UserId;
MailMessage msg = new MailMessage();
msg.Priority = MailPriority.High;
msg.To.Add(admin#example.com);
msg.Subject = "New User registration approval";
msg.Body = mailBody;
msg.IsBodyHtml = false;
SmtpClient smtp = new SmtpClient();
smtp.Send(msg);
msg.Dispose();
}
The above function takes a username param from the original registration action, which we use to find the user record (note: Username in the Users table is a UNIQUE column, so it will fail validation if it's not unique). I create the body of the email using plain text, and attach the URL of the approval action for the admin to confirm approval. Everything else is standard mail code (SMTP settings are stored in my web.config file).
Account controller -- Approval action:
public ActionResult Approval(int id)
{
var user = db.Users.Find(id);
user.IsApproved = true;
db.SubmitChanges();
EmailApproval(id);
return View(user);
}
Simple action that changes the "IsApproved" flag to true, which grants the user access to the application. The View that is returned contains basic user information displayed in a nice confirmation window. Lastly, there's a function "EmailApproval" which sends an email to the user about being approved. It uses the same emailing practices as the other function above.
Hope this helps.
You can make action insecure ([AllowAnonymous] will help you) and implement security for this action via IP address.
In this case you need to implement you own Authorization filter (read there how to do it) and check that it is called from predefined admin's IP and call is local (use HttpRequest.IsLocal for checking this).
Frankly, I never will do something insecure in my projects so you have to think twice before doing something similar for this.
From other hand you can extend your session timeout.
[This question relates to ASP.NET MVC4, and it is about best-practice approach - so please, don't suggest hacks.]
I want to authenticate users using an auth token sent in the request URL. It works similarly to a password reset token, except in this case it does not go to a reset page but instead grants access to some portion of the site. The idea is to send the URL with the auth token to a verified email address of the user. Users can click the link and perform some actions without typing their password.
Out-of-the-box, ASP.NET has the [Authorize] attribute and the SimpleMembershipProvider - these seem to work great, but they do some voodoo magic under the hood (like auto-generating database tables), so I don't know how to extend them to add this link-based auth token.
I don't expect an exact answer, but please do point me to the right direction.
Thanks!
Uf, broad question. But I will try at least to direct you to a right direction.
So first if suggest that you use Forms Authentication as a base, but you will have to customize using of it. And I presume that you do not want to use cookies for the authentication as this is native behaviour of the Forms Authentication.
The most important point you should consider to have it you custom query string token based authentication.
Create a login action and in this action you will authorize the user, if he have granted access you ask FormsAuthentication to create AuthCookie. For the further on you just take the httpCookie.Value as your auth token that you will carry in query string.
You need to implement the Application_BeginRequest in the Global.asax that will handle this query string tokens and translate it into the cookie. With this approach you can leverage all the ASP.NET Forms Authentication infrastructure.
This is quite high level picture w/o code. If you need more detail help I can also provide it to you.
You should just use a regular Action that accepts HttpGet.
Upon receiving the token, immediately invalid it so it can't be used again.
Also, only accept tokens that are within your pre-defined range of time period, like 24 or 72 hours.
Thank you Peter for idea.
If smb need to create JWT token authorization for old ASP.NET MVC5.I wrote small example. I don't serialize cookie to JWT. I create a JWT and after I am checking it in the BeginRequest. If everything is ok, I create a cookie and set it to the httpContext.Request. I used authentication mode="Forms" for application and it require cookies.
For create JWT token:
const string secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk";
[AllowAnonymous]
[HttpPost]
public ActionResult LoginJWT(LoginViewModel model)
{
ActionResult response = null;
if (ModelState.IsValid)
{
if (true) //todo: check user login&password
{
var payload = new Dictionary<string, object>
{
{ "iss", "subject" },
{ "sub", "api" },
{ "exp", DateTimeOffset.UtcNow.AddHours(2).ToUnixTimeSeconds()},
{ "iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds()},
{ "jti", Guid.NewGuid() },
{ "uid", "64" } //custom field for identificate user
};
IJwtAlgorithm algorithm = new HMACSHA256Algorithm(); // symmetric
IJsonSerializer serializer = new JsonNetSerializer();
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
var token = encoder.Encode(payload, secret);
response = Content(token);
}
else
{
response = new HttpStatusCodeResult(System.Net.HttpStatusCode.BadRequest, "Login or password are not found");
}
}
else
{
response = new HttpStatusCodeResult(System.Net.HttpStatusCode.BadRequest, "Errors in Model");
}
return response;
}
For check JWT token in Global.asax:
public override void Init()
{
this.BeginRequest += this.BeginRequestHandler;
base.Init();
}
private void BeginRequestHandler(object sender, EventArgs e)
{
var bearerToken = this.Context.Request.Headers["Authorization"];
if (bearerToken != null)
{
var token = bearerToken.StartsWith("Bearer ") ? bearerToken.Substring(7) : bearerToken;
const string secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk";
int userId = 0;
try
{
IJsonSerializer serializer = new JsonNetSerializer();
var provider = new UtcDateTimeProvider();
IJwtValidator validator = new JwtValidator(serializer, provider);
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
IJwtAlgorithm algorithm = new HMACSHA256Algorithm(); // symmetric
IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder, algorithm);
var json = decoder.DecodeToObject<IDictionary<string, string>>(token, secret, verify: true);
if (json.TryGetValue("uid", out var uid))
{
userId = Convert.ToInt32(uid);
}
}
catch (TokenExpiredException)
{
Console.WriteLine("Token has expired");
}
catch (SignatureVerificationException)
{
Console.WriteLine("Token has invalid signature");
}
if (userId != 0)
{
// check user by id, if found create cookie.
}
}
}
I used:
jwt-dotnet/jwt library 7.2.1
I'm trying to serve an iCalendar file (.ics) in my MVC application.
So far it's working fine. I have an iPhone subscribing to the URL for the calendar but now I need to serve a personalised calendar to each user.
When subscribing to the calendar on the iPhone I can enter a username and password, but I don't know how to access these in my MVC app.
Where can I find details of how the authentication works, and how to implement it?
It turns out that Basic Authentication is what is required. I half had it working but my IIS configuration got in the way. So, simply returning a 401 response when there is no Authorization header causes the client (e.g. iPhone) to require a username/password to subscribe to the calendar.
On the authorization of the request where there is an Authorization request header, the basic authentication can be processed, retrieving the username and password from the base 64 encoded string.
Here's some useful code for MVC:
public class BasicAuthorizeAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException("filterContext");
}
var auth = filterContext.HttpContext.Request.Headers["Authorization"];
if (!String.IsNullOrEmpty(auth))
{
var encodedDataAsBytes = Convert.FromBase64String(auth.Replace("Basic ", ""));
var value = Encoding.ASCII.GetString(encodedDataAsBytes);
var username = value.Substring(0, value.IndexOf(':'));
var password = value.Substring(value.IndexOf(':') + 1);
if (MembershipService.ValidateUser(username, password))
{
filterContext.HttpContext.User = new GenericPrincipal(new GenericIdentity(username), null);
}
else
{
filterContext.Result = new HttpStatusCodeResult(401);
}
}
else
{
if (AuthorizeCore(filterContext.HttpContext))
{
var cachePolicy = filterContext.HttpContext.Response.Cache;
cachePolicy.SetProxyMaxAge(new TimeSpan(0));
cachePolicy.AddValidationCallback(CacheValidateHandler, null);
}
else
{
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusDescription = "Unauthorized";
filterContext.HttpContext.Response.AddHeader("WWW-Authenticate", "Basic realm=\"Secure Calendar\"");
filterContext.HttpContext.Response.Write("401, please authenticate");
filterContext.HttpContext.Response.StatusCode = 401;
filterContext.Result = new EmptyResult();
filterContext.HttpContext.Response.End();
}
}
}
private void CacheValidateHandler(HttpContext context, object data, ref HttpValidationStatus validationStatus)
{
validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
}
}
Then, my controller action looks like this:
[BasicAuthorize]
public ActionResult Calendar()
{
var userName = HttpContext.User.Identity.Name;
var appointments = GetAppointments(userName);
return new CalendarResult(appointments, "Appointments.ics");
}
I found this really helpful, but i hit a few problems during the development and i thought i would share some of them to help save other people some time.
I was looking to get data from my web application into the calendar for an android device and i was using discountasp as a hosting service.
The first problem i hit was that the validation did not work when uploaded to the server, stangely enough it was accepting my control panel login for discountasp but not my forms login.
The answer to this was to turn off Basic Authentication in IIS manager. This resolved the issue.
Secondly, the app i used to sync the calendar to the android device was called iCalSync2 - its a nice app and works well. But i found that it only worked properly when the file was delivered as a .ics (duh for some reason i put it as a .ical.. it must have been late) and i also had to choose the webcal option
Lastly i found i had to add webcal:// to the start of my url instead of http://
Also be careful as the code posted above ignores the roles input variable and always passes nothing so you might need to do some role based checks inside your calendar routine or modify the code above to process the roles variable.
Many days ago, I asked this question and I'm set with a working offline OpenID provider (I guess).
What I want is a simple login text box for OpenID identifier which will automatically accept the user and consider the user as logged in, then I want him to be redirected to the main products page (Products => Index).
But, what I didn't know (and didn't find on the internet) is how to go on after the fake authentication process.
I tried to do this:
[HttpPost]
public ActionResult Login(string openid)//openid is the identifier taken from the login textbox
{
var rely = new OpenIdRelyingParty();
var req = rely.CreateRequest(openid);
req.RedirectToProvider();
return RedirectToAction("Index", "Products");
}
First of all, it is not being redirected in the 4th line (instead the login page is refreshed) and second, no authenticated user is really there, I mean when I checked User in Watch window, it is not null but Username is empty and there is no authenticated identity which, I guess, means that there is no cookies set.
P.S:
1. No exceptions are being thrown. But when I try to call FormsAuthentication.RedirectFromLogin() method I get an exception (Server cannot modify cookies after HTTP headers have been set).
2. Index action method is marked with [Authorize] attribute, and so when someone tries to browse Products it is redirected to the login screen, but when he logs in (fake login), shouldn't he be redirected back to Products page?
I tried this also:
[HttpPost]
public ActionResult Login(string openid)
{
var rely = new OpenIdRelyingParty();
return rely.CreateRequest(openid).RedirectingResponse.AsActionResult();
}
But no luck.
What did I miss? and how to make it work as expected? I can depend on myself but I need a decent documentation for OpenID especially for the offline local provider.
Check this example: OpenID and OAuth using DotNetOpenAuth in ASP.NET MVC
public ActionResult OpenId(string openIdUrl)
{
var response = Openid.GetResponse();
if (response == null)
{
// User submitting Identifier
Identifier id;
if (Identifier.TryParse(openIdUrl, out id))
{
try
{
var request = Openid.CreateRequest(openIdUrl);
var fetch = new FetchRequest();
fetch.Attributes.AddRequired(WellKnownAttributes.Contact.Email);
fetch.Attributes.AddRequired(WellKnownAttributes.Name.First);
fetch.Attributes.AddRequired(WellKnownAttributes.Name.Last);
request.AddExtension(fetch);
return request.RedirectingResponse.AsActionResult();
}
catch (ProtocolException ex)
{
_logger.Error("OpenID Exception...", ex);
return RedirectToAction("LoginAction");
}
}
_logger.Info("OpenID Error...invalid url. url='" + openIdUrl + "'");
return RedirectToAction("Login");
}
// OpenID Provider sending assertion response
switch (response.Status)
{
case AuthenticationStatus.Authenticated:
var fetch = response.GetExtension<FetchResponse>();
string firstName = "unknown";
string lastName = "unknown";
string email = "unknown";
if(fetch!=null)
{
firstName = fetch.GetAttributeValue(WellKnownAttributes.Name.First);
lastName = fetch.GetAttributeValue(WellKnownAttributes.Name.Last);
email = fetch.GetAttributeValue(WellKnownAttributes.Contact.Email);
}
// Authentication
FormsAuthentication.SetAuthCookie(userName: email, createPersistentCookie: false);
// Redirection
return RedirectToAction("Index", "Products");
case AuthenticationStatus.Canceled:
_logger.Info("OpenID: Cancelled at provider.");
return RedirectToAction("Login");
case AuthenticationStatus.Failed:
_logger.Error("OpenID Exception...", response.Exception);
return RedirectToAction("Login");
}
return RedirectToAction("Login");
}
Basically, your action method is called twice:
The first time by your form being submitted by the user.
The second time is a call back (redirect) from the OpenID provider.
This time, there will be a value for the response. If response.Status is valid, you can log your user in using the FormsAuthentication class and finally you can redirect him to your main products page.