We migrated a multitenant MVC application from ASP.NET Membership Provider to ASP.NET Identity.
This is my Startup.Auth.cs (simplified):
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity =
SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, Identity, int>(
TimeSpan.FromMinutes(30),
(manager, user) =>
manager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie),
clIdentity => clIdentity.GetUserId<int>())
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
}
In our multitenant application, each tenant has its own 'slug' (e.g. http://example.com/tenant1/ and http://example.com/tenant2/)
However, currently, the cookies are stored in the root. This causes security issues as users from tenant1 are automatically logged in on the website from tenant2.
How can we make the CookiePath (in CookieAuthenticationOptions) variable so that it changes depending on the tenant?
I fixed this issue with a lot of help from dampee.
The CookiePath in the CookieAuthenticationOptions object is evaluated only once: at application startup.
The easiest solution (workaround) was to create a derived CookieAuthenticationProvider that overrides ResponseSignIn and ResponseSignOut.
They both have an argument called context which has a property called CookiePath. Modify this property in both of these methods to change the CookiePath.
You can also use the class I created.
Then all you have to do is replace the CookieAuthenticationProvider in the CookieAuthenticationOptions with the one you just created.
This works for the ApplicationCookie. The ExternalSignInCookie doesn't matter that much since it is used only temporarily while signing in with an external login.
Improving on SamuelDebruyn's own solution, I found you can pass the path from the SignIn call to the provider using an AuthenticationProperties object. This way, instead of extracting the path from the request context as his gist shows, you can pass it explicitly from the source:
// method inside web api controller
private void SignIn(string name, string cookiePath)
{
var claims = new[] { new Claim(ClaimTypes.Name, name) };
var identity = new ClaimsIdentity(claims, "ApplicationCookie");
var options = new AuthenticationProperties();
options.Dictionary["CustomCookiePath"] = cookiePath;
var authManager = Request.GetOwinContext().Authentication;
authManager.SignIn(options, identity);
}
// Startup.cs
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
Provider = new CustomCookieProvider()
});
// custom provider
public class CustomCookieProvider : CookieAuthenticationProvider
{
public override void ResponseSignIn(CookieResponseSignInContext context)
{
context.CookieOptions.Path = context.Properties.Dictionary["CustomCookiePath"];
base.ResponseSignIn(context);
}
}
You can use a custom ICookieManager to dynamically return the cookie value to the CookieAuthenticationProvider based on whatever is in the request, to do this you would still maintain the CookiePath as "/" and then leave it up to the ICookieManager to return (or write) the cookie however you want. The CookieManager is an option on the CookieAuthenticationOptions. I blogged about this here: http://shazwazza.com/post/owin-cookie-authentication-with-variable-cookie-paths/
Related
In ASP.NET Core 6 MVC multi-tenant application tenants have different path base like /tenant1 and /tenant2.
Middleware sets HttpContext PathBase from request url.
SignInAsync method always sets authentication cookie path to the root path /.
I'm trying to set authentication cookie path from PathBase using this code snippet:
Path = Request.PathBase.HasValue ? Request.PathBase.Value : "/"
The code shown below throws a compile-time error since AuthenticationProperties does not have a Path property. How to set cookie Path property so that different users can authenticated with different base paths?
public class AccountController : Controller
{
public async Task<IActionResult> LogOn(string user, string password)
{
if (password != "secret")
throw new ApplicationException("Bad password");
var claims = new List<Claim> { new Claim(ClaimTypes.Name, user) };
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties
{
IsPersistent = true,
AllowRefresh = true,
// TODO: this throws compile error since Path property does not exist
Path = Request.PathBase.HasValue ? Request.PathBase.Value : "/"
};
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), authProperties);
}
}
You should be able to achieve this by writing your own ICookieManager and setting it when adding cookie authentication scheme.
ICookieManager methods get HttpContext as input so you have access to PathBase from there.
builder.Services.AddAuthentication()
.AddCookie("Cookie", options =>
{
options.CookieManager = CustomCookieManager();
});
Here's the default implementation of ICookieManager : ChunkingCookieManager
var claims = new[]
{
new Claim("UserID", user.ID.ToString()),
new Claim(ClaimTypes.Role, "pioneer")
};
var principal = new ClaimsPrincipal(
new ClaimsIdentity(
claims, CookieAuthenticationDefaults.AuthenticationScheme));
await HttpContext.Authentication.SignInAsync("Cookies", principal);
This is the cookie that I'm creating and using to sign in as per instruction from this tutorial: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie
The cookie is being created, but I don't know how to access the cookie or the data inside of it to get the user.ID value.
Once you sign via HttpContext.Authentication.SignInAsync, you should be able to access the User property in your controllers (it is part of ControllerBase), or HttpContext.User.
This User is a ClaimsPrincipal object that is created from the cookie automatically.
I use token auth for my WebApi application.
I have the following ConfigureAuth method in Startup class:
// Configure the application for OAuth based flow
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
// In production mode set AllowInsecureHttp = false
AllowInsecureHttp = true
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
and ApplicationOAuthProvider:
public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider
{
private readonly string _publicClientId;
public ApplicationOAuthProvider(string publicClientId)
{
if (publicClientId == null)
{
throw new ArgumentNullException("publicClientId");
}
_publicClientId = publicClientId;
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
var user = await userManager.FindAsync(context.UserName, context.Password);
//ApplicationUser user = new ApplicationUser() { UserName ="a" };
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager,
OAuthDefaults.AuthenticationType);
ClaimsIdentity cookiesIdentity = await user.GenerateUserIdentityAsync(userManager,
CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = CreateProperties(user.UserName);
AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
}
so, I should call /Token and pass credentials to get token. It works, but I want to create Unit Test for it. Is it possible?
The only way to do that is by make an integration test, which asserts the full pipeline testing - from request to response. Before the actual test on the server, you can call the token endpoint to get it, and then use it in the actual unit test by attaching it to the response. I have a sample, which uses MyTested.WebApi here:
Sample
You can do the same without the testing library, this is just how to do it.
I like the idea of pluggable configuration.
For Unit Test project, I want to use specific identity and get predictable data fro LDAP. So, i use the following line in my unit test method when setting http configuration:
config.Filters.Add(new WebApiSetIdentityFilter(config, identityName));
where the filter just "hacks" the identity, replacing the fields I need:
public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
//This principal flows throughout the request.
context.Principal = new GenericPrincipal(new GenericIdentity(this.IdentityName, "LdapAuthentication"), new string[0]);
}
We use to create email delegates through Google Email Settings API, but after the deprecation of OAuth 1.0 we were no longer able to authenticate properly. After doing some research I think we should create a service account, delegate domain-wide access for that service account, then authenticate using it. However I can't seem to get it to work, all I receive from Google is 401 unauthorized. Does someone know what I am doing wrong? Here is most of the code, I'm using .Net/c# and I'm using Google Apps for business.
ServiceAccountCredential credential = new ServiceAccountCredential(new ServiceAccountCredential.Initializer("serviceAccountEmail")
{
Scopes = new[] { "https://apps-apis.google.com/a/feeds/emailsettings/2.0/ " },
User = "admin email string"
}.FromCertificate({X509 certificate from service account p12 file}));
credential.RequestAccessTokenAsync(System.Threading.CancellationToken.None).Wait(-1);
GoogleMailSettingsService service = new GoogleMailSettingsService("domain name", "appname");
service.SetAuthenticationToken(credential.Token.AccessToken);
service.CreateDelegate("delegator", "delegate");
For those who may need this answer in the future, I was able to provide a solution through the following. For reference I am running a web app using MVC framework, but the solution could be tweaked for a console or GUI standalone app as well.
Basically, I was able to authenticate the GoogleMailSettingsService.Service.RequestFactory with a GOAuth2RequestFactory object.
For instance:
GoogleMailSettingsService service = new GoogleMailSettingsService("domain", "applicationName");
service.RequestFactory = new GOAuth2RequestFactory("service", "AppName", new OAuth2Parameters() { AccessToken = AuthorizationCodeWebApp.AuthResult.Credential.Token.AccessToken });
Now for the AuthorizationCodeWebApp.AuthResult I implemented the following:
public async Task<ActionResult> DelegationMenu(CancellationToken cancellationToken)
{
var result = await new AuthorizationCodeMvcApp(this, new AppFlowMetadata()).AuthorizeAsync(cancellationToken);
if (result.Credential == null)
return new RedirectResult(result.RedirectUri); //Will redirect to login page for Google Admin to authenticate.
Session["AuthResult"] = result;
return View();
}
public class AppFlowMetadata : FlowMetadata
{
private static readonly IAuthorizationCodeFlow flow =
new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = "ClientId",
ClientSecret = "ClientSecret"
},
Scopes = new[] { "https://apps-apis.google.com/a/feeds/emailsettings/2.0/" },
DataStore = new FileDataStore("C:\\OAuth2.0Tokens")
});
public override string GetUserId(Controller controller)
{
var user = controller.Session["user"];
if (user == null)
{
user = Guid.NewGuid();
controller.Session["user"] = user;
}
return user.ToString();
}
public override IAuthorizationCodeFlow Flow
{
get { return flow; }
}
}
A service account isn't required for this action. The Email Settings API, within the Admin SDK, allows a Super Admin to set a delegation for an account within the domain without the need to impersonate the user via a service account.
Check out this section of the Developers site for more information on this API. You can also test this on the OAuth Playground and add delegates right from there.
Hi I am new to working with Servicestack and have downloaded their very comprehensive bootstrapapi example and am working with it, but am still having some issues. The issue is with security, what is happening is I am getting 405 errors when trying to access the protected services. Using the authenticate service it appears that I am authenticating correctly. Please help and explain. Here is the code:
public class Hello
{
public string Name { get; set; }
}
public class AuthHello
{
public string Name { get; set; }
}
public class RoleHello
{
public string Name { get; set; }
}
public class HelloResponse
{
public string Result { get; set; }
}
The Services:
public class HelloService : ServiceBase<Hello>
{
//Get's called by all HTTP Verbs (GET,POST,PUT,DELETE,etc) and endpoints JSON,XMl,JSV,etc
protected override object Run(Hello request)
{
return new HelloResponse { Result = "Hello, Olle är en ÖL ål " + request.Name };
}
}
[Authenticate()]
public class AuthHelloService : RestServiceBase<AuthHello>
{
public object Execute(Hello request)
{
return new HelloResponse { Result = "Hello, " + request.Name };
}
}
[RequiredRole("Test")]
public class RoleHelloService : RestServiceBase<RoleHello>
{
public object Execute(Hello request)
{
return new HelloResponse { Result = "Hello, " + request.Name };
}
}
Here is the AppHost:
public class HelloAppHost : AppHostBase
{
//Tell Service Stack the name of your application and where to find your web services
public HelloAppHost() : base("Hello Web Services", typeof(HelloService).Assembly) { }
public override void Configure(Container container)
{
//Register all Authentication methods you want to enable for this web app.
Plugins.Add(new AuthFeature(() => new AuthUserSession(), new IAuthProvider[] {new CustomCredentialsAuthProvider(), //HTML Form post of UserName/Password credentials
}));
container.Register<ICacheClient>(new MemoryCacheClient() { FlushOnDispose = false });
//register user-defined REST-ful urls
Routes
.Add<Hello>("/hello")
.Add<Hello>("/hello/{Name}")
.Add<AuthHello>("/AuthHello")
.Add<RoleHello>("/RoleHello");
}
}
UPDATE
Everything works as expect if you replace : RestServiceBase with : ISevice so now the question is why.
Check the wiki documentation first
I would first go through the documentation in ServiceStack's Authentication Wiki to get a better idea about how ServiceStack's Authentication works. There's a lot of documentation in the wiki, so if you're unsure of something you should refer to that first. It's a community wiki so feel free to expand whats there if you think it can help others.
Refer to the implementation in the source code if behavior is not clear
If you're unsure of what something does you should refer to the RequiredRole source code as the master authority as how it works. RequiredRole is just a Request Filter Attribute which gets run before every service that has the attribute.
The RequiredRole attribute just calls your session.HasRole() method as seen here:
public bool HasAllRoles(IAuthSession session)
{
return this.RequiredRoles
.All(requiredRole => session != null
&& session.HasRole(requiredRole));
}
Because it just calls your session you can override the implementation of session.HasRole() if you have a custom session.
Registering and Implementing a CustomUserSession
The Social BootstrapApi project does implement its own CustomSession that it registers here but does not override the HasRole() implementation so it uses the built-in implementation in the base AuthUserSession.HasRole() which simply looks like the Roles collection to see if the user has the specified role in their Session POCO:
public virtual bool HasRole(string role)
{
return this.Roles != null && this.Roles.Contains(role);
}
Session properties populated by AuthUserRepository
The Roles property (as well as most other properties on a users Session) is populated by the AuthUserRepository that you have specified e.g. if you're using the OrmLiteAuthRepository like SocialBootstrapApi does here than the Roles attribute is persisted in the Roles column in the UserAuth RDBMS table. Depending on the AuthUserRepository you use the UserAuth / UserOAuthProvider POCOs get stored as RDBMS tables in OrmLite or as text blobs in Redis, etc.
Manage roles and permissions with AssignRoles / UnAssignRoles services
So for a user to have the required role (and authorization to pass), it should have this Role added to its UserAuth db row entry. ServiceStack's AuthFeature includes 2 services for managing users permissions and roles:
/assignroles
/unassignroles
How to initially give someone the Admin Role
These services does require a user with the Admin Role to be already authenticated.
You can do this by manually changing a specific users UserAuth.Role column to include the value "Admin". The Social Bootstrap API project instead does this by handling the OnAuthenticated() event on its CustomUserSession that simply checks to see if the authenticated username is declared in the Web.Config and if it is, calls the AssignRoles service giving that authenticated user the Admin Role:
if (AppHost.Config.AdminUserNames.Contains(session.UserAuthName)
&& !session.HasRole(RoleNames.Admin))
{
var assignRoles = authService.ResolveService<AssignRolesService>();
assignRoles.Execute(new AssignRoles {
UserName = session.UserAuthName,
Roles = { RoleNames.Admin }
});
}