I'm having trouble solving architecture of an ASP MVC application that servers html pages and web services through ServiceStack.
The application lives in the base url eg "http://myapplication.com" and SS lives in "http://myapplication.com/api" because it is the easiest way to configure both.
In general everything works fine, but when I reached the part of the authorization and authentication, is where I'm stuck.
For one, I need the application handle cookies as ASP normally do FormsAuthentication through, and users would go through a login screen and could consume actions and controllers when the attribute "Authorize" is used. This is typical of ASP, so I have no problem with it, such as "http://myapplication.com/PurchaseOrders".
On the other hand, clients of my application will consume my web service api from javascript. Those web services will also be tagged in some cases with the attribute "Authenticate" of ServiceStack. For example "http://myapplication.com/api/purchaseorders/25" would have to validate if the user can view that particular purchase order, otherwise send a 401 Unauthorized so javascript can handle those cases and display the error message.
Last but not least, another group of users will make use of my API by a token, using any external application (probably Java or .NET). So I need to solve two types of authentication, one using username and password, the other by the token and make them persistant so once they are authenticated the first time, the next calls are faster to solve from the API.
This is the code that I have so far, I've put it very simply to make clear the example.
[HttpPost]
public ActionResult Logon(LogOnModel model, string returnUrl)
{
if (ModelState.IsValid)
{
JsonServiceClient client = new JsonServiceClient("http://myapplication.com/api/");
var authRequest = new Auth { provider = CredentialsAuthProvider.Name, UserName = model.UserName, Password = model.Password, RememberMe = model.RememberMe };
try
{
var loginResponse = client.Send(authRequest);
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(loginResponse.UserName, false, 60);
var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(ticket));
Response.Cookies.Add(cookie);
if (Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1 && returnUrl.StartsWith("/") && !returnUrl.StartsWith("//") && !returnUrl.StartsWith("/\\"))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Test");
}
}
catch (Exception)
{
ModelState.AddModelError("", "Invalid username or password");
}
}
return View();
}
As for the authentication provider I am using this class
public class MyCredentialsAuthProvider : CredentialsAuthProvider
{
public MyCredentialsAuthProvider(AppSettings appSettings)
: base(appSettings)
{
}
public override bool TryAuthenticate(IServiceBase authService, string userName, string password)
{
//Add here your custom auth logic (database calls etc)
//Return true if credentials are valid, otherwise false
if (userName == "testuser" && password == "nevermind")
{
return true;
}
else
{
return false;
}
}
public override void OnAuthenticated(IServiceBase authService, IAuthSession session, IOAuthTokens tokens, Dictionary<string, string> authInfo)
{
//Fill the IAuthSession with data which you want to retrieve in the app eg:
session.FirstName = "some_firstname_from_db";
//...
session.CreatedAt = DateTime.Now;
session.DisplayName = "Mauricio Leyzaola";
session.Email = "mauricio.leyzaola#gmail.com";
session.FirstName = "Mauricio";
session.IsAuthenticated = true;
session.LastName = "Leyzaola";
session.UserName = "mauricio.leyzaola";
session.UserAuthName = session.UserName;
var roles = new List<string>();
roles.AddRange(new[] { "admin", "reader" });
session.Roles = roles;
session.UserAuthId = "uniqueid-from-database";
//base.OnAuthenticated(authService, session, tokens, authInfo);
authService.SaveSession(session, SessionExpiry);
}
}
On the Configure function of AppHost I am setting my custom authentication class to use it as the default. I guess I should create another class and add it here as well, to handle the token scenario.
Plugins.Add(new AuthFeature(() => new CustomUserSession(),
new IAuthProvider[] {
new MyCredentialsAuthProvider(appSettings)
}, htmlRedirect: "~/Account/Logon"));
So far, ServiceStack is working as expected. I can submit a post to /auth/credentials passing username and password and it stores this information, so next call to a service the request is already authorized, great so far!
The question I need to know is how to call (and probably set somewhere in SS) the user that is logging in from my Account controller. If you see the first block of code I am trying to call the web service (looks like I am doing it wrong) and it works, but the next call to any web service looks unauthenticated.
Please don't point me to ServiceStack tutorials, I've been there for the last two days and still cannot figure it out.
Thanks a lot in advance.
Here is what I usually use:
You can replace the "Logon" action method with the code below:
public ActionResult Login(LogOnModel model, string returnUrl)
{
if (ModelState.IsValid)
{
try
{
var authService = AppHostBase.Resolve<AuthService>();
authService.RequestContext = System.Web.HttpContext.Current.ToRequestContext();
var response = authService.Authenticate(new Auth
{
UserName = model.UserName,
Password = model.Password,
RememberMe = model.RememberMe
});
// add ASP.NET auth cookie
FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
return RedirectToLocal(returnUrl);
}
catch (HttpError)
{
}
}
// If we got this far, something failed, redisplay form
ModelState.AddModelError("", "The user name or password provided is incorrect.");
return View(model);
}
...and the plugins:
//Default route: /auth/{provider}
Plugins.Add(new AuthFeature(() => new CustomUserSession(),
new IAuthProvider[] {
new CustomCredentialsAuthProvider(),
new CustomBasicAuthProvider()
}));
....the Auth provider classes:
public class CustomCredentialsAuthProvider : CredentialsAuthProvider
{
public override bool TryAuthenticate(IServiceBase authService, string userName, string password)
{
return UserLogUtil.LogUser(authService, userName, password);
}
}
public class CustomBasicAuthProvider : BasicAuthProvider
{
public override bool TryAuthenticate(IServiceBase authService, string userName, string password)
{
return UserLogUtil.LogUser(authService, userName, password);
}
}
...finally, the logging utility class
internal static class UserLogUtil
{
public static bool LogUser(IServiceBase authService, string userName, string password)
{
var userService = new UserService(); //This can be a webservice; or, you can just call your repository from here
var loggingResponse = (UserLogResponse)userService.Post(new LoggingUser { UserName = userName, Password = password });
if (loggingResponse.User != null && loggingResponse.ResponseStatus == null)
{
var session = (CustomUserSession)authService.GetSession(false);
session.DisplayName = loggingResponse.User.FName.ValOrEmpty() + " " + loggingResponse.User.LName.ValOrEmpty();
session.UserAuthId = userName;
session.IsAuthenticated = true;
session.Id = loggingResponse.User.UserID.ToString();
// add roles and permissions
//session.Roles = new List<string>();
//session.Permissions = new List<string>();
//session.Roles.Add("Admin);
//session.Permissions.Add("Admin");
return true;
}
else
return false;
}
}
Related
I have implemented google authentication in my mvc site. Here is my sample code-
AuthConfig.cs
public static class AuthConfig
{
private static string GoogleClientId = ConfigurationManager.AppSettings["GoogleClientId"];
private static string GoogleClientSecret = ConfigurationManager.AppSettings["GoogleClientSecret"];
public static void RegisterAuth()
{
GoogleOAuth2Client clientGoog = new GoogleOAuth2Client(GoogleClientId, GoogleClientSecret);
IDictionary<string, string> extraData = new Dictionary<string, string>();
OpenAuth.AuthenticationClients.Add("google", () => clientGoog, extraData);
}
}
Global.asax
AuthConfig.RegisterAuth();
AccountController.cs
public ActionResult RedirectToGoogle()
{
string provider = "google";
string returnUrl = "";
return new ExternalLoginResult(provider, Url.Action("ExternalLoginCallback", new { ReturnUrl = returnUrl }));
}
[AllowAnonymous]
public ActionResult ExternalLoginCallback(string returnUrl)
{
string ProviderName = OpenAuth.GetProviderNameFromCurrentRequest();
if (ProviderName == null || ProviderName == "")
{
NameValueCollection nvs = Request.QueryString;
if (nvs.Count > 0)
{
if (nvs["state"] != null)
{
NameValueCollection provideritem = HttpUtility.ParseQueryString(nvs["state"]);
if (provideritem["__provider__"] != null)
{
ProviderName = provideritem["__provider__"];
}
}
}
}
GoogleOAuth2Client.RewriteRequest();
var redirectUrl = Url.Action("ExternalLoginCallback", new { ReturnUrl = returnUrl });
var retUrl = returnUrl;
var authResult = OpenAuth.VerifyAuthentication(redirectUrl);
string ProviderDisplayName = OpenAuth.GetProviderDisplayName(ProviderName);
if (authResult.IsSuccessful)
{
string ProviderUserId = authResult.ProviderUserId;
}
return Redirect(Url.Action("Index", "User"));
}
This code is working fine. But I want to restrict the user to sign-in with his/her organizational account like "abc#example.com". Where I can specify the hosted domain property? When I created app id and secret for this app from google dev console, I saw Verify domain tab. Do I need to add my organizational domain here?
You can sort of. You can specify the hd (Hosted Domain) parameter within the Authentication URI parameters.
hd - OPTIONAL - The hd (hosted domain) parameter streamlines the login process for G Suite hosted accounts. By including the domain of the G Suite user (for example, mycollege.edu), you can indicate that the account selection UI should be optimized for accounts at that domain. To optimize for G Suite accounts generally instead of just one domain, use an asterisk: hd=*.
Don't rely on this UI optimization to control who can access your app, as client-side requests can be modified. Be sure to validate that the returned ID token has an hd claim value that matches what you expect (e.g. mycolledge.edu). Unlike the request parameter, the ID token claim is contained within a security token from Google, so the value can be trusted.
I have an ActionFilter name of Log that it log user ip and other details when user login to the website so for do this work I write following code:
public class Log : ActionFilterAttribute
{
public IAppUserManager UserManager { get; set; }
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
var status = filterContext.Controller.TempData.Any(pair => pair.Key == "status" && (int)pair.Value == 200);
if (filterContext.HttpContext.User != null && filterContext.HttpContext.User.Identity.IsAuthenticated && status)
{
var logIp = new AddIpAddressDto()
{
Browser = filterContext.HttpContext.Request.GetBrowser(),
Ip = filterContext.HttpContext.Request.GetIp(),
Os = filterContext.HttpContext.Request.UserAgent.GetOs(),
UrlReferrer = filterContext.HttpContext.Request.UrlReferrer?.ToString(),
UserId = Guid.Parse(filterContext.HttpContext.User.Identity.GetUserId()),
UserName = filterContext.HttpContext.User.Identity.GetUserName(),
};
UserManager.Log(logIp);
}
base.OnResultExecuted(filterContext);
}
}
this code work when that filterContext.HttpContext.User.Identity.IsAuthenticated is ture.
The Log Filter declare on Login Action:
[AllowAnonymous]
[Route("sign-in", Name = "signInRoute")]
[HttpPost, ValidateAntiForgeryToken]
[Log]
public virtual async Task<ActionResult> Login(LoginDto login, string returnTo)
{
var signInStatus = await _signInManager
.PasswordSignInAsync(user.UserName, login.Password, login.RememberMe, true)
.ConfigureAwait(false);
switch (signInStatus) // is Success
{
case SignInStatus.Success:
TempData["status"] = 200;
return RedirectToLocal(returnTo);
case SignInStatus.LockedOut:
// todo return time of louckout
break;
case SignInStatus.RequiresVerification:
return RedirectToAction("ConfirmEmail");
case SignInStatus.Failure:
return View(CleanPassWordInLogin(login));
default:
throw new ArgumentOutOfRangeException();
}
}
Login Action Works fine and signInStatus is Success but after excuted Action IsAuthenticated is false.
To solve this issue I've tried the following items:
Used HttpContext.Current.GetOwinContext();
Defined following code in IoC (StructureMap 4.5.2)
config.For<HttpContextBase>().Use(() => new HttpContextWrapper(HttpContext.Current));
Tried OnActionExecuted,OnActionExecuting,OnResultExecuting
Used IAuthenticationManager in Identity 2.0
How Can I Solve this issue?
After the execution of SignInManager.PasswordSignInAsync, the authentication cookie will be created which includes the user info. So User.Identity info will be filled by the claims from the authentication cookie, which are not parsed yet (this cookie will be parsed in the second request to the server, not in the same login request). That's why you can't use User.Identity just after PasswordSignInAsync. At this specific point, you have only one option to find the userId:
string userId = UserManager.FindByName(model.Email)?.Id;
In my ServiceStack app I would like to deny access to channels for unauthorized users - so even the join event would not fire for an unauthorized client. I am using custom auth provider that does not interact with the DB and is very minimalistic for now (mainly for testing purposes)
public class RoomsAuthProvider : CredentialsAuthProvider
{
private int userId = 0;
public RoomsAuthProvider(AppSettings appSettings) : base(appSettings)
{
}
public RoomsAuthProvider()
{
}
public override bool TryAuthenticate(IServiceBase authService,
string userName, string password)
{
if (password == "ValidPassword")
{
return true;
}
else
{
return false;
}
}
public override IHttpResult OnAuthenticated(IServiceBase authService,
IAuthSession session, IAuthTokens tokens,
Dictionary<string, string> authInfo)
{
//Fill IAuthSession with data you want to retrieve in the app eg:
session.FirstName = "some_firstname_from_db";
//...
//Call base method to Save Session and fire Auth/Session callbacks:
return base.OnAuthenticated(authService, session, tokens, authInfo);
//session.CreatedAt = DateTime.Now;
//session.DisplayName = "CustomDisplayName" + userId;
//session.IsAuthenticated = true;
//session.UserAuthName = session.UserName;
//session.UserAuthId = userId.ToString();
//Interlocked.Increment(ref userId);
//authService.SaveSession(session, SessionExpiry);
//return null;
}
}
Main service piece:
[Authenticate]
public class ServerEventsService : Service
{
...
}
sidenote - I have tried overriding the default DisplayUsername to not be username1...usernameN but no luck. My client code is
var client = new ServerEventsClient("http://localhost:1337/", "home")
{
OnConnect = OnConnect,
OnCommand = HandleIncomingCommand,
OnMessage = HandleIncomingMessage,
OnException = OnException,
OnHeartbeat = OnHeartbeat
}.Start();
client.Connect().Wait();
var authResponse = client.Authenticate(new Authenticate
{
provider = "credentials",
UserName = "test#gmail.com",
Password = "p#55w0rd",
RememberMe = true,
});
client.ServiceClient.Post(new PostChatToChannel
{
Channel = "home", // The channel we're listening on
From = client.SubscriptionId, // Populated after Connect()
Message = "Hello, World!",
});
Even if I skip the authenticate call the other clients will still get onJoin command about not authenticated client when it tries to do an unauthorized post (and get an error). Also when I intentionally do multiple unauthorized users counter grows - assigned username becomes username2, username3 and so on - how can I disable unauthorized users COMPLETELY? Marking my DTOs with Authenticate also didn't change anything. Any ideas are welcome as well as crytics as I'm new to ServiceStack and would like to implement the best practices.
There's already an option to limit access to authenticated users only with:
Plugins.Add(new ServerEventsFeature {
LimitToAuthenticatedUsers = true
});
I'm working on an MVC 6 application that does not use Entity or Identity. Instead, I'm using Dapper. I have a controller that accepts a POST request and uses Dapper to check the database to see if the users name / password match. All I'd like to do is store the users name and whether they're logged in or not so I can make that check on subsequent pages.
I looked around and it seems like using Cookie based authentication should allow me to do what I want. Here's the relevant code in my Startup.cs file:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
LoginPath = "/account/login",
AuthenticationScheme = "Cookies",
AutomaticAuthenticate = true,
AutomaticChallenge = true
});
Here's what the relevant code in my controllers login action looks like:
var user = _repo.FindByLogin(model.VendorId, model.Password);
if (user != null) {
var claims = new List < Claim > {
new Claim("VendorId", user.VendorId),
new Claim("Name", "john")
};
var id = new ClaimsIdentity(claims, "local", "name", "role");
await HttpContext.Authentication.SignInAsync("Cookies", new ClaimsPrincipal(id));
var l = ClaimsIdentity.DefaultNameClaimType;
return RedirectToAction("Index", "PurchaseOrders");
}
The above code seems to work in that a cookie is being saved, but I'm not sure how I would go about getting the user information back out of the cookie (or even how to retrieve the cookie on subsequent requests in the first place).
In my mind I'm imagining being able to do something like: var user = (User)HttpContext.Request.Cookies.Get(????), but I'm not sure if that's practical or not.
You can get the user data back by using the ClaimsPrincipal.FindFirstValue(xxx)
here is my example class which can be used in Controller/Views to get the current user information
public class GlobalSettings : IGlobalSettings
{
private IHttpContextAccessor _accessor;
public GlobalSettings(IHttpContextAccessor accessor)
{
_accessor = accessor;
}
public string RefNo
{
get
{
return GetValue(_accessor.HttpContext.User, "employeeid");
}
}
public string SAMAccount
{
get
{
return GetValue(_accessor.HttpContext.User, ClaimTypes.WindowsAccountName);
}
}
public string UserName
{
get
{
return GetValue(_accessor.HttpContext.User, ClaimTypes.Name);
}
}
public string Role
{
get
{
return GetValue(_accessor.HttpContext.User, ClaimTypes.Role);
}
}
private string GetValue(ClaimsPrincipal principal, string key)
{
if (principal == null)
return string.Empty;
return principal.FindFirstValue(key);
}
}
Example Usage in controller after DI:
var currentUser = GlobalSettings.SAMAccount;
Please note that you need to inject HttpContextAccessor in ConfigureServices method in Startup.cs
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
I have two projects, MVC web app and a web service.
In the MVC web app I have a controller called 'Account'.
In 'Account' there is an action called 'Login'.
The 'Login'action gets the Authentication Manager from the request.
The 'Login' action calls the web service to identify if the user exists.
If the user exists, the web service will return 'ClaimsIdentity' for that specific user.
The 'Login'action will then use the authentication manager and the claims identity to sign the user in.
After this, the user is not signed/authenticated. I know this because User.Identity.Name is blank and User.Identity.IsAuthenticated is false. Need explanation of why this is happening and what I can do to solve the issue. There are no build-time or run-time errors. I need the web service to perform ALL calls to the database.
Account.Login Code
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult LogIn(LoginModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var authManager = GetAuthenticationManager();
var loginFeedback = _webService.Login(model);
if (loginFeedback.Success)
{
authManager.SignIn(loginFeedback.Identity);
return RedirectToAction("Index", "Home");
}
else
{
ModelState.AddModelError("", loginFeedback.FeedbackMessage);
return View(model);
}
}
GetAuthenticationManager
private IAuthenticationManager GetAuthenticationManager()
{
var context = Request.GetOwinContext();
return context.Authentication;
}
Web Service Login
public LoginFeedbackDto Login(LoginModel loginModel)
{
var userManager = GetUserManager();
var user = userManager.Find(loginModel.Email, loginModel.Password);
var dto = new LoginFeedbackDto();
string status = user.Status.ToLower();
if (status == "invalid")
{
dto.Success = false;
dto.FeedbackMessage = #"This account has not been activated.";
}
else if (status == "rejected")
{
dto.Success = false;
dto.FeedbackMessage = #"This account has been rejected.";
}
else if (status != "invalid" && status != "rejected" && status != "processed")
{
dto.Success = false;
dto.FeedbackMessage = #"We are unable to log you in due to a technical issue.";
}
else
{
dto.Success = true;
dto.Identity = userManager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie);
}
userManager.Dispose();
return dto;
}
What is happening here is the identity you are creating is being 'lost' on the RedirectToAction.
You could make use of CookieMiddleware, which can create a cookie representing the current user.
You will need an Owin startup class which will need code something like the following.
Make sure 'SelfIssue' is the same AuthenticationType that your webservice sets the ClaimsIdentity.AuthenticationType to.
public const string SelfIssue = "Self-Issue";
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(SelfIssue);
app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = SelfIssue });
}