I'm building a multitenant application that also needs to sign guest users (users added as guests, not anonymous). The guest must be able to access resources in the tenant. For example:
User X from Tenant A is a guest in Tenant B. Must be able to access resources in Tenant B
User Y is an account from an external provider. It is also a guest in Tenant B. Must be able to access resources in Tenant B
User Z is an account from an external provider. It is a guest in Tenant C. Must be able to access resources in Tenant C
Each user selects the tenant they want to interact with, so obtaining the tenantId is not an issue.
Microsoft Identity Framework really simplifies the configuration for the most common cases, but I can't find a way to configure the application with multitenant AND guest users.
My code is fairly standard:
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
Configuration.Bind("AzureAd", options);
}
)
And in the controller
[HttpGet]
[AuthorizeForScopes(Scopes = new string[] { GraphScope.FilesReadAll })]
public async Task<IActionResult> Index(string driveId, string tenantId)
{
var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(new string[] { GraphScope.FilesReadAll }, tenantId: tenantId);
//etc
}
I mainly followed the examples from here
Main problems:
[AuthorizeForScopes] uses static scopes, so it is impossible to use a scope from another tenant (can't do for example {tenantUrl}/.default)
I can't use the "common" authority (as in "https://login.microsoftonline.com/common") since I need users to get claims from a specific tenant.
I was able to use a flow similar the one for AuthorizeForScopes while obtaining the token. My idea was to return a ChallengeResult action with the specific authority, but I can't find information on how to configure it, or if that's even possible.
try
{
var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync ...
}
catch (someAuthException)
{
return ChallengeResult ...
}
Is there a way to do so, or is there a simpler way to accomplish what I try to do?
Related
I'm creating a ASP.NET Core 5 MVC project and i'm using Openid-Connect with Microsoft to allow users in our organization to login this webservice.
This all works fine, users can authenticate with their microsoft work account and get signed in. Now i would like to add their profile picture to the claim.
However, I can't seem to get my IClaimsTransformation to work. I've made the UserPhotoClaims to try and add the profile picture for the logged in user. If i use the same graph code to get the picture inside a controller, then it works fine.
If i do it inside a ClaimsTransformation then it fails with the following message:
MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call.
Microsoft.Identity.Client.Internal.Requests.Silent.SilentRequest.ExecuteAsync(CancellationToken cancellationToken)
MicrosoftIdentityWebChallengeUserException: IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. See https://aka.ms/ms-id-web/ca_incremental-consent.
Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForUserAsync(IEnumerable scopes, string authenticationScheme, string tenantId, string userFlow, ClaimsPrincipal user, TokenAcquisitionOptions tokenAcquisitionOptions)
My ClaimsTransformation looks like this:
public class UserPhotoClaims : IClaimsTransformation
{
private readonly GraphServiceClient _graph;
public UserPhotoClaims(GraphServiceClient graphServiceClient)
{
_graph = graphServiceClient;
}
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
if(!principal.HasClaim(x => x.Type == "photo"))
{
ClaimsIdentity id = new ClaimsIdentity();
Stream photo = await _graph.Me.Photo.Content.Request().GetAsync();
string ps = Convert.ToBase64String((photo as MemoryStream).ToArray());
id.AddClaim(new Claim("photo", ps));
principal.AddIdentity(id);
}
return principal;
}
}
And i've added the transformation in my ConfigureServices in startup.cs:
services.AddTransient<IClaimsTransformation, UserPhotoClaims>();
Any help much appreciated!
This issue may arise due to permissions not being consented ,as they are already done and token cache is not called again.
Possible workarounds:
Please check if the permissions in the portal are granted consent and try again after adding other scopes in both code and portal and consent them .
Clear the cookies , sign out and retry.
Try by adding [AuthorizeForScopes(Scopes = new[] { "<TheScopeThatYouArePassingOnAcquireTokenMethod>" })] to your action in controller .It processes the MSAL.NET MsalUiRequiredException to handle conditional access and incremental consent.
Please check this for more details
You may try by Handling the exception using try-catch >see this
In public client applications ,the design of MSAL.NET is such that AcquireTokenInteractive never looks at the cache. you may need to call AcquireTokenSilent first
try
{
result = await app.AcquireTokenSilent(scopes, accounts.FirstOrDefault())
.ExecuteAsync();
}
catch (MsalUiRequiredException ex)
{
6. Please check this transforming claims in ASP.NET Core | Microsoft Docs
and compare the code.
Please check this tutorial that can guide in detail
References:
msal-net-acquire-token-silently | Microsoft docs
msal-error-handling | Microsoft docs
get-microsoft-account-profile-photo-after-login | SO reference
You may raise a support ticket if workarounds doesn't resolve the issue.
I've been developing an Angular app with .NET Core backend (services). The task is to enable an integrated authentication, i.e. make it work with the local user seamlessly, so I login to my (connected to a local AD) machine once and the web application lets me in without the necessity to login a second time. We've been working with Identity Server 4 and intended to implement this scenario using it.
There is a little documentation on the official website concerning the Windows Authentication (e.g. against Active directory): http://docs.identityserver.io/en/latest/topics/windows.html but it doesn't explain much. As per my info, to make this scenario work the browser utilizes either Kerberos or NTLM. Neither of them is mentioned in the IS4 docs. I'm lacking the understanding of how the local credentials are getting picked up and how IS4 'knows' the user belongs to AD? How I can make sure only the users from a specific domain have access to my app?
I found some working stuff here https://github.com/damienbod/AspNetCoreWindowsAuth but questions remain the same. Even though I was able to get to the app with my local account I don't understand the flow.
I expect the user utilizing the app in the local network to log-in to the app without entering the login/password (once he's already logged in to the Windows). Is this something achievable?
Identity Server is intended to serve as an Identity Provider, if you need to talk with your AD you should see the Federation Gateway architecture they propose using the IAuthenticationSchemeProvider. Where Identity Server acts as an endpoint and talks with your AD.
This is the link:
http://docs.identityserver.io/en/latest/topics/federation_gateway.html
You have the control to programmatically reach your AD and pass the correct credentials to get the authentication. That step should be done in your Identity Server. After you get authenticated you should get redirected to your application again.
About your last question, the answer is yes, if you have your website hosted on an intranet and you have the access to your AD, you don't need to capture your credentials as user input, you can programmatically reach the AD as I said.
Bellow is the code I use to connect with my active directory
On the ExternalController class, you get when you use IdentityServer, you have this:(I don't remember at the top of my head how much I changed from the original code, but you should get the idea)
/// <summary>
/// initiate roundtrip to external authentication provider
/// </summary>
[HttpGet]
public async Task<IActionResult> Challenge(string provider, string returnUrl)
{
if (string.IsNullOrEmpty(returnUrl)) returnUrl = "~/";
// validate returnUrl - either it is a valid OIDC URL or back to a local page
if (Url.IsLocalUrl(returnUrl) == false && _interaction.IsValidReturnUrl(returnUrl) == false)
{
// user might have clicked on a malicious link - should be logged
throw new Exception("invalid return URL");
}
if (AccountOptions.WindowsAuthenticationSchemeName == provider)
{
// windows authentication needs special handling
return await ProcessWindowsLoginAsync(returnUrl);
}
else
{
// start challenge and roundtrip the return URL and scheme
var props = new AuthenticationProperties
{
RedirectUri = Url.Action(nameof(Callback)),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", provider },
}
};
return Challenge(props, provider);
}
}
private async Task<IActionResult> ProcessWindowsLoginAsync(string returnUrl)
{
// see if windows auth has already been requested and succeeded
var result = await HttpContext.AuthenticateAsync(AccountOptions.WindowsAuthenticationSchemeName);
if (result?.Principal is WindowsPrincipal wp)
{
// we will issue the external cookie and then redirect the
// user back to the external callback, in essence, testing windows
// auth the same as any other external authentication mechanism
var props = new AuthenticationProperties()
{
RedirectUri = Url.Action("Callback"),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", AccountOptions.WindowsAuthenticationSchemeName },
}
};
var id = new ClaimsIdentity(AccountOptions.WindowsAuthenticationSchemeName);
id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.Identity.Name));
id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name));
// add the groups as claims -- be careful if the number of groups is too large
if (AccountOptions.IncludeWindowsGroups)
{
var wi = wp.Identity as WindowsIdentity;
var groups = wi.Groups.Translate(typeof(NTAccount));
var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value));
id.AddClaims(roles);
}
await HttpContext.SignInAsync(
IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme,
new ClaimsPrincipal(id),
props);
return Redirect(props.RedirectUri);
}
else
{
// trigger windows auth
// since windows auth don't support the redirect uri,
// this URL is re-triggered when we call challenge
return Challenge(AccountOptions.WindowsAuthenticationSchemeName);
}
}
If you want to use Azure AD, I would recommend you to read this article:
https://damienbod.com/2019/05/17/updating-microsoft-account-logins-in-asp-net-core-with-openid-connect-and-azure-active-directory/
Not sure if it's what you want, but I would use the Active Directory Federation Services to configure an OAuth2 endpoint and obtain the user token in the .Net Core Web App.
Isn't NTLM authentication support limited on non Microsoft browsers?
OAuth2 have the advantage of using only standard technologies.
One way to do it is to have 2 instances of the app deployed.
The first one is configured to use Windows Authentication and the other one uses IS4.
ex:
yoursite.internal.com
yoursite.com
Your local DNS should redirect traffic internally from yoursite.com to yoursite.internal.com
yoursite.internal.com will be the one configured to use AD authentication. You should have a flag in your appsettings.json to indicate if this instance is a AD auth or IS4 auth.
The downside of this solution is that you have to deploy 2 instances
so im trying to create a SaaS application with ASP.NET Boilerplate, and i come into some problem as follows:
As i observe the framework, i noted that the "RegisterAsync" function in UserRegistrationManager create user based on the currently active tenant. It means if i currently log in on tenant '1', then when i register new user, the new user will have tenantId '1'. On the other hand, when i currently not logged in, if i register a new user, the app will show exception 'cannot register host user'.
public async Task<User> RegisterAsync(string name, string surname, string emailAddress, string phoneNumber, string userName, string plainPassword, bool isEmailConfirmed)
{
CheckForTenant();
var tenant = await GetActiveTenantAsync();
var user = new User
{
TenantId = tenant.Id,
Name = name,
Surname = surname,
EmailAddress = emailAddress,
PhoneNumber = phoneNumber,
IsActive = true,
UserName = userName,
IsEmailConfirmed = isEmailConfirmed,
Roles = new List<UserRole>()
};
return user;
}
private void CheckForTenant()
{
if (!AbpSession.TenantId.HasValue)
{
throw new InvalidOperationException("Can not register host users!");
}
}
The application that i want to build requires the function for new user to be able to sign up along with free trial and then paid subscription. So i think that the new user should be able to create tenant by themself. So if the new user register, they will be forced to create new tenant before they can do any other thing in the app.
The problem is that the tenantId column in User table cannot be null, so i can register without tenant. Im thinking of assign all newly created user to 'Default' tenant at first, but i think that this was not the best practices.
Is there any way to overcome this problem or any references about that? Thanks in advance!
Based on my empirical SaaS Application development experience, a typical Self-Signup flow in Multi-Tenant applications would be like the one given below
User opts to self-signin
Allow the user to pick a subscription plan (most likely a trial plan)
Get the Company Name (tenant name) as part of the signup flow
Create a new tenant that has the subscription (2)
Add the signup user as the administrator for that tenant
In case of a trial plan, set up the suitable request handler to keep validating if the tenant has crossed the subscribed number of trial days, in that case, force redirect to payment page or signout
If the user has opted to Signup for a paid subscription (during signup), after provisioning the tenant go to the payment page. Once payment succeeds, capture the transactionid and allow the user to login and use the application.
The flow that you wanted to be using is straightforward
Build a custom self-signup process, obtain the company name (Tenant Name)
Also capture the emailid of the user that is performing the sign-up
Create the tenant based on info from (1)
Set the administrator for the tenant based on the info from (2)
All your API calls should be working fine.
Note
Have a separate Self-Signup Service like (TenantSelfRegistrationService) so that you can allow anonymous access to that service.
In terms of security, set captcha and set rate-limits or CSRF Tokens etc to enforce security in the signup process.
Hope this clarifies
I looked at the code and the documentation and I think you should never allow an unknown user to create new tenants. This should happen by a person who has the correct authorization to create tenants. This is a user that exists in the host tenant.
You as admin in the host tenant need to create tenant for somebody else and add them as admin for that tenant.
Registering users is then done through the normal way with the register webpage running for that tenant.
How to do that, I leave to you to figure out with the documentation of boilerplate itself! Documentation
Using Owin + Oauth2 + Identity2.
I have a web Api with default basic authentication setup that i have modified.
my startup.cs partial class
public void ConfigureAuth(IAppBuilder app)
{
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);//TODO: prob wont need this
// 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"),//TODO: prob wont need this
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
// In production mode set AllowInsecureHttp = false
AllowInsecureHttp = true //TODO: set debug mode
};
// Token Generation
app.UseOAuthBearerTokens(OAuthOptions);
}
my startup.cs class partial at the root
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
ConfigureAuth(app);
WebApiConfig.Register(config);
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseWebApi(config);
}
my applicationOAuthProvider.cs
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
//get user
var service = new CarrierApi.CarrierManagementClient();
var result = service.LoginAsync(context.UserName, context.Password);
var user = result.Result.Identity;
//TODO: log stuff here? i.e lastlogged etc?
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
ClaimsIdentity oAuthIdentity = user;
ClaimsIdentity cookiesIdentity = user;
AuthenticationProperties properties = CreateProperties(user.GetUserName());
AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
}
As you can see i actually go and get the identity via a wcf call to our existing DB. when using postman, i get the /token url and obtain my bearer token, on the next request i pass it into the header and call my controller method.
[Authorize(Roles = "Templates_Access")]
public string Post([FromBody]string value)
{
return "woo";
}
This works great, if the user has the permission it wont allow access, if they do it does.
However if i go to our website that uses the same wcf and DB and change a users permission, if i send the same request on postman it still allows access even though ive removed that permission on the role the user is assigned too.
How do i make sure that permissions are "refreshed" or checked again on each request?
Every roles of a user logged in are stored in the bearer token at login time as claim, in the GrantResourceOwnerCredentials method. If a request have to be authorized, the role is searched on the list stored in the bearer token by the default implementation of AuthorizationFilter; so if you change the user's permissions, you need a new login.
This behavior respects the Stateless contraint of a Restfull architecture, as Fielding wrote in his dissertation, and also this is a good balance between performance and security
If you need a different behavior there is more than one possibility.
Refresh Token
You Can use Refresh Token, implementing the GrantRefreshToken method of applicationOAuthProvider class; you can retrieves the refreshed user's permissions and create a new access token; this is a good article to learn how.
Keep in mind:
more complexity on client
no real time effect; you have to wait the access Token expiring
If the Access Token has a short life, you have to update it often (even when the user permissions are not changed) otherwise a long life does not solve the problem
Check Permissions each request
You can implement a custom AuthorizationFilter and check in the database the permissions of the user, but it is a slow solution.
Cache and Login Session
You can generate a user session's key (like a guid) for each login in the GrantResourceOwnerCredentials method, and store it in the bearer token as a claim. You have to store it also in a cache system (like Redis), using two index: the user session's key and the userId. The official documentation of Redis explains how.
When the permessions of a user are changed, you can invalidate every sessions of that user in the cache system, searching by userId
You can implement a custom AuthorizationFilter and check for each request in cache if the session is valid, searching by user session's key.
Be careful: this will violate the stateless constraint and your architecture will not restfull
Here you can find the standard implementation of AuthorizaAttribute filter.
You can create your custom filter extending AuthorizeAttribute and overriding the IsAuthorized method.
Most likely there are other ways, but how often are changed the permissions of a user? In many systems, also in systems where the security is the first requirement, if the permissions's configuration of a user is changed during an active session, it is necessary a new login to active the new one.
Are you sure you needs to modify this standard behavior?
If you are, I suggest the solution with a cache system.
I'm developing ASP.NET 5 application using Identity 2.0. I have two types of users:
Normal - they authenticate using standard login method.
Temporary - they should login based on provided token.
I do not want to store temporary users, except from information required to authenticate user (some username and token). If the user provides username and valid password he should be logged in.
I'm not sure how to achieve this.
You could use Identity in both scenarios simultaneously as well. For first scenario use Identity just like you have done before without any change but for second scenario you a slight modify in login method.
public ActionResoult TempLogin(string username, string password)
{
// imaging you have own temp user manager, completely independent from identity
if(_tempUserManager.IsValid(username,password))
{
// user is valid, going to authenticate user for my App
var ident = new ClaimsIdentity(
new[]
{
// adding following 2 claim just for supporting default antiforgery provider
new Claim(ClaimTypes.NameIdentifier, username),
new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", "ASP.NET Identity", "http://www.w3.org/2001/XMLSchema#string"),
// an optional claim you could omit this
new Claim(ClaimTypes.Name, username),
// you could even add some role
new Claim(ClaimTypes.Role, "TempUser"),
new Claim(ClaimTypes.Role, "AnotherRole"),
// and so on
},
DefaultAuthenticationTypes.ApplicationCookie);
// Identity is sign in user based on claim don't matter
// how you generated it Identity
HttpContext.GetOwinContext().Authentication.SignIn(
new AuthenticationProperties { IsPersistent = false }, ident);
// auth is succeed,
return RedirectToAction("MyAction");
}
ModelState.AddModelError("", "We could not authorize you :(");
return View();
}
Since we injected our logic to Identity, we don't need to do extra thing at all.
[Authorize]
public ActionResult MySecretAction()
{
// all authorized users could use this method don't matter how has been authenticated
// we have access current user principal by calling also
// HttpContext.User
}
[Authorize(Roles="TempUser")]
public ActionResult MySecretAction()
{
// just temp users have accesses to this method
}
You'll need to extend the ASP.NET Identity Libraries, using your custom logic and/or storage.
Here you can find an example in my Github account with some useful links that I used to read when I was trying to understand the ASP.NET Identity stuff: https://github.com/hernandgr/AspNetIdentityDemo
Hope it helps!