I want to enable Multitenant Authentication. My Code is in ASP.Net Webforms and Here is the StartUp.cs file code.
public partial class Startup
{
const string MSATenantId = "XXXXXXXXXXXXXXX";
private static string clientId = ConfigurationManager.AppSettings["ida:ClientID"];
private static string aadInstance = EnsureTrailingSlash(ConfigurationManager.AppSettings["ida:AADInstance"]);
private static string authority = aadInstance + "common";
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions { });
// instead of using the default validation (validating against a single issuer value, as we do in line of business apps),
// we inject our own multitenant validation logic
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuers = new List<string>()
{
"https://sts/windows.net/XXXXXXXXXXXX"
}
// If the app needs access to the entire organization, then add the logic
// of validating the Issuer here.
// IssuerValidator
},
Notifications = new OpenIdConnectAuthenticationNotifications()
{
SecurityTokenValidated = (context) =>
{
//if (context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value != MSATenantId)
//{
// context.HandleResponse();
// context.Response.Redirect("InvalidUser.aspx");
//}
// If your authentication logic is based on users
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{
// Pass in the context back to the app
context.HandleResponse();
// Suppress the exception
return Task.FromResult(0);
}
},
});
// This makes any middleware defined above this line run before the Authorization rule is applied in web.config
app.UseStageMarker(PipelineStage.Authenticate);
}
//private Task OnSecurityTokenValidatedAsync(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
//{
// // Make sure that the user didn't sign in with a personal Microsoft account
// if (notification.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value == MSATenantId)
// {
// notification.HandleResponse();
// notification.Response.Redirect("/Account/UserMismatch");
// }
// return Task.FromResult(0);
//}
}
I want only the user with the MSATenantId should able to access the application for that I have read there are multiple ways I have tried below two though both are not working:
In this the application doesn't redirect to the Home page
TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuers = new List<string>()
{
"https://sts/windows.net/XXXXXXXXXX"
}
// If the app needs access to the entire organization, then add the logic
// of validating the Issuer here.
// IssuerValidator
},
In this it doesn't redirect to invalid page.
SecurityTokenValidated = (context) =>
{
if (context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value != MSATenantId)
{
context.HandleResponse();
context.Response.Redirect("InvalidUser.aspx");
}
If your authentication logic is based on users
return Task.FromResult(0);
},
Am I missing anything or do I need to add something in the above scenarios. I want to just test with one Tenant first and then I'll add more tenant.
Also, how does the 1 and 2 are different ?
This works if I don't use any of the above option. I am able to login with Azure account.
Your question has been resolved, add it as the answer to the end of the question.
Your issuer is set incorrectly, you should change it to: https://sts.windows.net/XXXXXXXXXXXX/.
I am able to resolve the issue. Issue was this url was incorrect https://sts/windows.net/XXXXXXXXXXXX
The correct URL is - https://sts.windows.net/XXXXXXXXXXXX/
TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuers = new List<string>()
{
"https://sts.windows.net/XXXXXXXXXX/"
}
// If the app needs access to the entire organization, then add the logic
// of validating the Issuer here.
// IssuerValidator
},
Related
I am trying to implement Identityserver4 (version 4.0.0) with windows authentication. While running on visual studio its working correctly. When I deploy this to IIS windows popup is showing continuously (401 status) after entering credentials. Below is my code . I also tried to deploy Duende Software's sample source also but getting the same result. I think there is some configuration missing from my end. Kindly help me.
Program.cs
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSerilog()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
launchSettings.json
"windowsAuthentication": true,
ExternalController.cs
public async Task<IActionResult> Challenge(string scheme, string returnUrl)
{
if (string.IsNullOrEmpty(returnUrl)) returnUrl = "~/";
if(scheme == "Windows")
{
return await ChallengeWindowsAsync(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");
}
// start challenge and roundtrip the return URL and scheme
var props = new AuthenticationProperties
{
RedirectUri = Url.Action(nameof(Callback)),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", scheme },
}
};
return Challenge(props, scheme);
}
//ChallengeWindowsAsync
private async Task<IActionResult> ChallengeWindowsAsync(string returnUrl)
{
// see if windows auth has already been requested and succeeded
var result = await HttpContext.AuthenticateAsync("Windows");
if (result?.Principal is WindowsPrincipal wp)
{
// we will issue the external cookie and then redirect the
// user back to the external callback, in essence, treating windows
// auth the same as any other external authentication mechanism
var props = new AuthenticationProperties()
{
RedirectUri = Url.Action("Callback"),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", "Windows" },
}
};
var id = new ClaimsIdentity("Windows");
// the sid is a good sub value
id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.FindFirst(ClaimTypes.PrimarySid).Value));
// the account name is the closest we have to a display 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
var wi = wp.Identity as WindowsIdentity;
// translate group SIDs to display names
var groups = wi.Groups.Translate(typeof(NTAccount));
var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value));
id.AddClaims(roles);
await HttpContext.SignInAsync(
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("Windows");
}
}
IIS Configuration
Windows authentication is enabled
I have API which makes use of JWTBearerAuthentication. Everything works fine from Authentication perspective. However there is a need to update the Claims on the principal after the validation is complete. There are some information which I have to derive instead of receiving from source (token) claims. I was thinking if there is a way that after the validation is successful I can add some claims to existing claims identity/ principal to perform some access rights stuff. Below is the sample code in my start up class.
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
TokenHandler = new ValidateJwtSecurityTokenHandlerforLogs(_loggingService, _environment),
AuthenticationMode = AuthenticationMode.Active,
TokenValidationParameters = new TokenValidationParameters()
{
NameClaimType = JwtClaimTypes.Name,
RoleClaimType = OpenIDConfiguration.Permission,
ValidateAudience = false,
ValidIssuer = authority,
IssuerSigningKeyResolver = (token, securityToken, identifier, parameters) =>
{
var discoveryDocument = Task.Run(() => configurationManager.GetConfigurationAsync()).GetAwaiter().GetResult();
return discoveryDocument.SigningKeys;
}
}
});
You have several options to modify the claims after authentication:
One option is
options.Events = new JwtBearerEvents()
{
OnTokenValidated = context =>
{
// Token has passed validation and a ClaimsIdentity has been generated.
context.Principal.Identities.First().AddClaim(new Claim("VIPCustomer", "YES"));
return Task.CompletedTask;
}
};
To enable more advanced claims transformation scenarios, we can add a custom transformation class
public class BonusLevelClaimTransformation : IClaimsTransformation
{
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
if (!principal.HasClaim(c => c.Type == "bonuslevel"))
{
//Lookup bonus level.....
principal.Identities.First().AddClaim(new Claim("bonuslevel", "12345"));
}
return Task.FromResult(principal);
}
}
Then we register it in Startup.cs:
services.AddTransient<IClaimsTransformation, BonusLevelClaimTransformation>();
I have following in my startup:
public partial class Startup
{
private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
private static string aadInstance = ConfigurationManager.AppSettings["ida:AadInstance"];
private static string tenant = ConfigurationManager.AppSettings["ida:Tenant"];
private static string redirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
// B2C policy identifiers
// public static string SignUpPolicyId = ConfigurationManager.AppSettings["ida:SignUpPolicyId"];
public static string SignInUpPolicyId = ConfigurationManager.AppSettings["ida:SignInUpPolicyId"];
public static string DefaultPolicy = SignInUpPolicyId;
public static string ResetPasswordPolicyId = ConfigurationManager.AppSettings["ida:ResetPasswordPolicyId"];
public static string ProfilePolicyId = ConfigurationManager.AppSettings["ida:UserProfilePolicyId"];
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
// Configure OpenID Connect middleware for each policy
app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(SignInUpPolicyId));
//app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(ResetPasswordPolicyId));
// app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(SignInPolicyId));
}
private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
notification.HandleResponse();
// Handle the error code that Azure AD B2C throws when trying to reset a password from the login page
// because password reset is not supported by a "sign-up or sign-in policy"
if (notification.ProtocolMessage.ErrorDescription != null && notification.ProtocolMessage.ErrorDescription.Contains("AADB2C90118"))
{
// If the user clicked the reset password link, redirect to the reset password route
notification.Response.Redirect("/Account/ResetPassword");
}
else if (notification.Exception.Message == "access_denied")
{
notification.Response.Redirect("/");
}
else
{
notification.Response.Redirect("/Home/Error?message=" + notification.Exception.Message);
}
return Task.FromResult(0);
}
private Task OnRedirectToIdentityProvider(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
var policy = notification.OwinContext.Get<string>("Policy");
if (!string.IsNullOrEmpty(policy) && !policy.Equals(DefaultPolicy))
{
//notification.ProtocolMessage.Scope = OpenIdConnectScopes.OpenId;
//notification.ProtocolMessage.ResponseType = OpenIdConnectResponseTypes.IdToken;
notification.ProtocolMessage.IssuerAddress = notification.ProtocolMessage.IssuerAddress.Replace(DefaultPolicy, policy);
}
return Task.FromResult(0);
}
private OpenIdConnectAuthenticationOptions CreateOptionsFromPolicy(string policy)
{
return new OpenIdConnectAuthenticationOptions
{
// For each policy, give OWIN the policy-specific metadata address, and
// set the authentication type to the id of the policy
MetadataAddress = String.Format(aadInstance, tenant, policy),
AuthenticationType = policy,
// These are standard OpenID Connect parameters, with values pulled from web.config
ClientId = clientId,
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed
},
Scope = "openid",
ResponseType = "id_token",
// This piece is optional - it is used for displaying the user's name in the navigation bar.
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
SaveSigninToken = true //important to save the token in boostrapcontext
}
};
}
}
In my Controller I have following:
public void ResetPassword()
{
// Let the middleware know you are trying to use the reset password
policy (see OnRedirectToIdentityProvider in Startup.Auth.cs)
HttpContext.GetOwinContext().Set("Policy", Startup.ResetPasswordPolicyId);
// Set the page to redirect to after changing passwords
var authenticationProperties = new AuthenticationProperties { RedirectUri = "/" };
HttpContext.GetOwinContext().Authentication.Challenge(authenticationProperties);
}
Right now the reset password just redirect to "/". I am not sure how this works, and havent been able to find any samples. I tried following this sample but it uses some cores libraries and havent succeeded using the documentation.
https://learn.microsoft.com/en-us/azure/active-directory-b2c/active-directory-b2c-devquickstarts-web-dotnet-susi
After following Chris suggestion it worked. See image below when clicking reset password.
Follow this Startup.Auth.cs file as closely as possible to get yourself started.
The ConfigureAuth method of the Startup class registers the OWIN OpenID Connect middleware that enables an ASP.NET MVC controller to set the Azure AD B2C policy, to be redirected to, using the OWIN context.
Example:
HttpContext.GetOwinContext().Set("Policy", Startup.ResetPasswordPolicyId);
I want to set an expiry time for my web app so that after 1 hour the user will automatically be logged out. It seems that azure now have a new feature called 'conditional access'. Firstly I have to have a premium account (so yet more money) and secondly I cant find anything in the documentation that shows how to use it to log someone out after a specified time. Has anyone used this feature yet ? how can this be done ?
can anyone help ?
Assuming that you are using OpenID Connect and Cookie authentication middleware to protect your web app, for your requirement, I assumed that you could add a custom claim named loggedTicks and check the time interval under the OnValidateIdentity of CookieAuthenticationProvider, then explicitly invoke the sign out operation against your web application and AAD. Here is the code snippet, you could refer to it:
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions() {
ExpireTimeSpan=TimeSpan.MaxValue,
Provider = new CookieAuthenticationProvider()
{
OnValidateIdentity = ctx => {
var loggedClaim=ctx.Identity.FindFirst("loggedTicks")?.Value;
if (loggedClaim != null)
{
var loggedDateTime = new DateTime(long.Parse(loggedClaim), DateTimeKind.Utc);
if (loggedDateTime.AddHours(1) < DateTime.UtcNow)
{
ctx.RejectIdentity();
ctx.OwinContext.Authentication.SignOut(OpenIdConnectAuthenticationDefaults.AuthenticationType, CookieAuthenticationDefaults.AuthenticationType);
}
}
return Task.FromResult(0);
}
}
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
RedirectUri = postLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = context =>
{
context.HandleResponse();
context.Response.Redirect("/Error?message=" + context.Exception.Message);
return Task.FromResult(0);
},
SecurityTokenValidated = async (x) =>
{
var identity = x.AuthenticationTicket.Identity;
//add a additional claim which represents the current user logged UTC time ticks
identity.AddClaim(new System.Security.Claims.Claim("loggedTicks", DateTime.UtcNow.Ticks.ToString()));
await Task.FromResult(0);
}
}
});
}
I have got a basic Identity Server setup as per the UI sample project instructions on GitHub. I have it set it up to use Windows authentication with our on site AD. This is working beautifully.
My issue is with adding the users AD groups to the claims. As per the sample project I have enabled the IncludeWindowsGroups option. Which seems to be adding the claims to the ClaimsIdentity. However, on my MVC client, when I print out the claims I only ever get the same 4. They are sid, sub, idp and name. I have tried adding other claims but I can never get any others to show up.
I have the following as my Client Setup:
return new List<Client>
{
// other clients omitted...
// OpenID Connect implicit flow client (MVC)
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.Implicit,
// where to redirect to after login
RedirectUris = { "http://localhost:5002/signin-oidc" },
// where to redirect to after logout
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
},
RequireConsent = false
}
};
Hopefully I am just missing something simple but I am struggling for ideas now, so any pointers would be much appreciated.
I managed to get this working with a few changes, beyond setting IncludeWindowsGroups = true in the IdentityServer4 project. Note that I downloaded the IdentityServer4 UI quickstart as of the 2.2.0 tag
Per this comment in GitHub, I modified ExternalController.cs in the quickstart UI:
// this allows us to collect any additonal claims or properties
// for the specific prtotocols used and store them in the local auth cookie.
// this is typically used to store data needed for signout from those protocols.
var additionalLocalClaims = new List<Claim>();
var roleClaims = claims.Where(c => c.Type == JwtClaimTypes.Role).ToList();
if (roleClaims.Count > 0)
{
additionalLocalClaims.AddRange(roleClaims);
}
I then created a profile service to copy the claims from Windows Auth into the token being sent back:
public class ProfileService : IProfileService
{
private readonly string[] _claimTypesToMap = {"name", "role"};
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
foreach (var claimType in _claimTypesToMap)
{
var claims = context.Subject.Claims.Where(c => c.Type == claimType);
context.IssuedClaims.AddRange(claims);
}
return Task.CompletedTask;
}
public Task IsActiveAsync(IsActiveContext context)
{
context.IsActive = true; //use some sort of actual validation here!
return Task.CompletedTask;
}
}
and registered with IdentityServer4 in Startup.cs
services
.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryIdentityResources(StaticConfig.GetIdentityResources())
.AddInMemoryApiResources(StaticConfig.GetApiResources())
.AddInMemoryClients(StaticConfig.GetClients())
.AddTestUsers(StaticConfig.GetUsers())
.AddProfileService<ProfileService>();
In my client config in IdentityServer4, I set user claims to be included in the Id token. I found that if I tried to map the claims in the callback to UserInfo, that context was lost in IdentityServer4, so the claims wouldn't map.
public static class StaticConfig
{
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
...
AlwaysIncludeUserClaimsInIdToken = true,
...
}
}
}
}
Finally, in Startup.cs for the client website, I did not setup the UserInfo callback; I just made sure that my name and role claims were mapped. Note that if your profile service returns any other claim types, you need to manually map them with a call to a helper method on options.ClaimActions.
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.SaveTokens = true;
options.ResponseType = "code id_token";
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
//map any other app-specific claims we're getting from IdentityServer
options.ClaimActions.MapUniqueJsonKey("someotherclaimname", "someotherclaimname");
};