thinktecture identity server v3 managing users in Asp.Net Identity v2 - c#

I want to use Identity Server v3 as a central authentication point for several internal web apps and I want to use Asp.Net Identity 2 as my repo for users and associated claims. I already have the two wired up together and I can see the Asp.Net Identity database created and populated when I authenticate with one of the social providers.
Update:
I can't get the Identity Manager UI to render. When I try to navigate to https://localhost:44333/#/users/create, it just displays the content of /index.html
The Thinktecture Identity Manager packages have been installed as required but I can’t find my way to the UI.
here is my Configuration method from the Startup class in my Host project:
public void Configuration(IAppBuilder app)
{
app.Map("/core", coreApp =>
{
var factory = new Thinktecture.IdentityManager.Host.AspNetIdentityIdentityManagerFactory("AspId");
coreApp.UseIdentityManager(new IdentityManagerConfiguration()
{
IdentityManagerFactory = factory.Create,
});
var idsrvOptions = new IdentityServerOptions
{
IssuerUri = "https://idsrv3.com",
SiteName = "Thinktecture IdentityServer v3 - beta 3",
Factory = Factory.Configure("AspId"),
SigningCertificate = Cert.Load(),
CorsPolicy = CorsPolicy.AllowAll,
CspOptions = new CspOptions
{
ReportEndpoint = EndpointSettings.Enabled,
},
AuthenticationOptions = new AuthenticationOptions
{
IdentityProviders = ConfigureIdentityProviders,
}
};
coreApp.UseIdentityServer(idsrvOptions);
});
This is probably very simple. Any help greatly appreciated.
Scott

my url should have been /core/#/users/create. It works now.

Related

Sustainsys.Saml2 - Saml2/Acs endpoint returns Error 500 when processing SSO

I've created a C# ASP .Net Core 6.0 application, and trying to implement SSO with Azure AD using Sustainsys.Saml2, specifically with the Sustainsys.Saml2.AspNetCore2 package. Having tested the implementation on my development machine with localhost, I can see it works as expected and authenticates the user, populates the Identity model, and redirects to correct URL.
However, when deployed into the test environment, using a dockerized version, the behaviour changes. When triggering SSO, the user is authenticated successfully in Azure, but when returning to the app, it returns an Error 500 at the Saml2/Acs endpoint. Reviewing the logs show no indication of any errors, and instead report successful authentication for the user.
The Program.cs configuration:
builder.Services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = "Saml2";
})
.AddSaml2(options =>
{
var logger = new LoggerFactory();
options.SPOptions.Logger = new AspNetCoreLoggerAdapter(logger.CreateLogger<Saml2Handler>());
options.SPOptions.EntityId = new EntityId(AppConfig.Saml_EntityID);
options.IdentityProviders.Add(
new IdentityProvider(new EntityId(AppConfig.Saml_AzureID), options.SPOptions)
{
Binding = Saml2BindingType.HttpRedirect,
LoadMetadata = true,
MetadataLocation = AppConfig.Saml_Metadata,
DisableOutboundLogoutRequests = false,
AllowUnsolicitedAuthnResponse = true
});
options.SPOptions.PublicOrigin = new Uri(AppConfig.BaseUrl);
options.SPOptions.ReturnUrl = new Uri(AppConfig.BaseUrl);
options.SPOptions.WantAssertionsSigned = true;
options.SPOptions.AuthenticateRequestSigningBehavior = SigningBehavior.Always;
options.SPOptions.ServiceCertificates.Add(new X509Certificate2(AppConfig.Saml_Cert_Path));
})
.AddCookie();
While troubleshooting the issue, I stumbled across some confusing behaviour that may or may not indicate what may be the issue. If I follow the following steps, I can end up at a point where the user is authenticated and can use the applications:
Click 'Login' to trigger the Saml authentication.
Hit the Error 500 at Saml2/Acs.
Click 'refresh', and 'continue' to resubmit the request.
The browser then continues to the intended URL, but says 'Connection Refused'
Use the browser back buttons to return to the application home screen, and refresh the page... Viola! Logged in!
Furthermore, when inspecting the request headers on the Saml2/Acs endpoint, I can see a Saml response is returned, which I can manually decode from base64 and read the correct information!
As mentioned, the logs don't mention any errors, just:
Initiating login to https://sts.windows.net/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/
and
Successfully processed SAML response _ba082bb8-7d2c-4aa4-a7dc-b1520312d084 and authenticated a*******#********.com
Any assistance, or guidance to a resolution would be much appreciated!
Maybe this is not relevant for .Net Core, but for .net framework 4.8
there was the following issues:
ReturnUrl of Service Provider was wrong: http://locahost/mysite/saml2/acs instead of correct one http://locahost/mysite/ (with trailing slash). Because of this, there was indefinite loop to http://locahost/mysite/saml2/acs`.
SPOptions spOptions = new SPOptions() { EntityId = new EntityId(spMetadataUrl), ReturnUrl = new Uri(hostUrl + "/"), DiscoveryServiceUrl = new Uri(hostUrl + #"/DiscoveryService"), Organization = organization, AuthenticateRequestSigningBehavior = SigningBehavior.Never, RequestedAuthnContext = requestedAuthnContext, Logger = logger, PublicOrigin = hostUri };
DO NOT USE UseExternalSignInCookie mehod, otherwise ClaimPrincipal will not set for current Thread (cookies will be parsed to claims, although latter will not be set, this can be checked with code below):
Saml2AuthenticationOptions options = CreateSaml2Options(configuration, certificate); options.SPOptions.Saml2PSecurityTokenHandler = new MySaml2PSecurityTokenHandler();
public class MySaml2PSecurityTokenHandler : Sustainsys.Saml2.Saml2P.Saml2PSecurityTokenHandler
{
protected override ClaimsIdentity CreateClaimsIdentity(Saml2SecurityToken samlToken, string issuer, TokenValidationParameters validationParameters)
{
ClaimsIdentity identity = base.CreateClaimsIdentity(samlToken, issuer, validationParameters);
Claim claim = new Claim("Name", "jon.doe");
Claim[] claims = new Claim[] { claim };
identity.AddClaims(claims);
return identity;
}
}
Additionally only for Net Framework 4.8, because of owin vs System.Web cookies bug, CookieManager should be used. Code:
var cookieManager = new Microsoft.Owin.Host.SystemWeb.SystemWebCookieManager();
Saml2AuthenticationOptions options = CreateSaml2Options(configuration, certificate);
CookieAuthenticationOptions cookieAuthentication = new CookieAuthenticationOptions()
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString(configuration.ServiceProviderSignOnUrl),
CookieManager = сookieManager,
ReturnUrlParameter = GetBase() + "/",
Provider = new CookieAuthenticationProvider()
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = (context) =>
{
var newIdentity = new ClaimsIdentity(context.Identity);
int newcount = 1;
newIdentity.AddClaim(new Claim("SIMPLECOUNT", newcount.ToString()));
context.ReplaceIdentity(newIdentity);
return Task.FromResult<object>(null);
}
}
};
app.UseCookieAuthentication(cookieAuthentication);
app.SetDefaultSignInAsAuthenticationType(cookieAuthentication.AuthenticationType);
//app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseSaml2Authentication(options);

.NET Core WebApi + Angular Authentication & Authorization via applications in Microsoft Azure

We have 2 web applications on Angular (FE_1, FE_2) and 3 API applications on .NET Core (see picture)
Need to login at once time from one site and working between two without any additional authorization processes
I mean, when we log in to site1 and receive token #1, I want this token to work with API_2 as well, and vice versa, when we log in to site2 and receive token #2, I want to use this token to work as well with API_1
So my question is how to properly configure applications in Azure and configure them internally based on the described architecture ?
Thankyou Manish. Posting your suggestion as an answer to help the other community members.
from the provided architecture in BE code add the list of valid audiences as per the below image make it to true
Below is the sample code where you can check the valid audience code.
services.AddAuthentication(cfg =>
{
cfg.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
cfg.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(opt =>
{
opt.Authority = "https://login.microsoftonline.com/common";
opt.Audience = "api://A134d6c8-8078-2924-9e90-98cef862eb9a"; // Set this to the App ID URL for the web API, which you created when you registered the web API with Azure AD.
opt.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidAudiences = new List<String>
{
// you could add a list of valid audiences
"A134d6c8-8078-2924-9e90-98cef862eb9a"
},
ValidIssuers = new List<string>
{
// Add tenant id after https://sts.windows.net/
"https://sts.windows.net/{YourTenantId}"
}
};
opt.Events = new JwtBearerEvents()
{
OnAuthenticationFailed = AuthenticationFailed
};
});
For complete information check the SO.

How to grant access to users externally authenticated and not registered in my WebApp using asp.net core 2.2?

Summary
This is my first try with OAuth2 and External Login Mechanisms.
I'm creating a WebApp that will expose API features through a user-friendly UI.
In order to make API calls, I need to receive an access token from QBO that grants access to resources.
So, my WebApp has an external login option which I use to authenticate against QBO, and then authorize my app.
Everything works fine until...
Services Configuration
Based on a tutorial for GitHub authentication, I came up with this.
services.AddAuthentication(o => {
o.DefaultAuthenticateScheme = IdentityConstants.ExternalScheme;
o.DefaultSignInScheme = IdentityConstants.ExternalScheme;
o.DefaultChallengeScheme = IdentityConstants.ExternalScheme;
})
.AddOAuth("qbo", "qbo", o => {
o.CallbackPath = new PathString("/signin-qbo");
o.ClientId = Configuration["ecm.qbo.client-id"];
o.ClientSecret = Configuration["ecm.qbo.client-secret"];
o.SaveTokens = true;
o.Scope.Add("openid");
o.Scope.Add("profile");
o.Scope.Add("email");
o.Scope.Add("com.intuit.quickbooks.accounting");
o.AuthorizationEndpoint = Configuration["ecm.qbo.authorization-endpoint"];
o.TokenEndpoint = Configuration["ecm.qbo.token-endpoint"];
o.UserInformationEndpoint = Configuration["ecm.qbo.user-info-endpoint"];
o.Events.OnCreatingTicket = async context => {
var companyId = context.Request.Query["realmid"].FirstOrDefault() ?? throw new ArgumentNullException("realmId");
var accessToken = context.AccessToken;
var refreshToken = context.RefreshToken;
Configuration["ecm.qbo.access-token"] = accessToken;
Configuration["ecm.qbo.refresh-token"] = refreshToken;
Configuration["ecm.qbo.realm-id"] = companyId;
context.Backchannel.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
context.Backchannel.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var response = await context.Backchannel.GetStringAsync(context.Options.UserInformationEndpoint);
var result = JsonConvert.DeserializeObject<Dictionary<string, string>>(response);
var user = (ClaimsIdentity)context.Principal.Identity;
user.AddClaims(new Claim[] {
new Claim("access_token", accessToken),
new Claim("refresh_token", refreshToken),
new Claim(ClaimTypes.GivenName, result["givenName"]),
new Claim(ClaimTypes.Surname, result["familyName"]),
new Claim(ClaimTypes.Email, result["email"]),
new Claim(ClaimTypes.Name, result["givenName"]+" "+result["familyName"])
});
};
});
This works. I can add my claims based on user information, the context.Principal.Identity indicates that it's authenticated.
For some reasons, it seems to try and redirect to `/Identity/Account/Login?returnUrl=%2F. Why is that?
Login page redirection
Here, I don't get why I get the redirection and this confuses me a lot. So, I added the AccountController just to try and shut it up.
namespace ecm.backoffice.Controllers {
[Authorize]
[Route("[controller]/[action]")]
public class AccountController : Controller {
[AllowAnonymous]
[HttpGet]
public IActionResult Login(string returnUrl = "/") {
return Challenge(new AuthenticationProperties { RedirectUri = returnUrl });
}
[Authorize]
[HttpGet]
public async Task<IActionResult> Logout(string returnUrl = "/") {
await Request.HttpContext.SignOutAsync("qbo");
return Redirect(returnUrl);
}
}
}
And this creates more confusion than it solves, actually. I'm lost here...
Apply Migrations
This Individual User Authentication WebApp seems to use Identity which looks like it creates a lot of behind the scene mechanisms. I first tried to register to my app, and had to "Apply Migrations", which I totally get, since the data model wasn't initialized.
So, I clicked the Apply Migrations button. I though I was okay with this...
Entity Framework Core
I am aware that the app is using Entity Framework Core for its persistence mechanism, hence the registration process, etc. And to configure it, I needed to add these lines to the services configs.
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>()
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<ApplicationDbContext>();
It looks like it just won't work at all.
Thoughts
At this point, I think that the message says it Entity Framework just can't load the user information from its underlying data store. I totally understand that, and I don't want to register this user. I just want to take for granted that if QBO authenticated the user, it's fine by me and I grant open bar access to the WebApp features, even if the user ain't registered.
How to tell that to my WebApp?
Related Q/A I read prior to ask
Prevent redirect to /Account/Login in asp.net core 2.2
ASP.NET Core (2.1) Web API: Identity and external login provider
asp.net core 2.2 redirects to login after successful sign in
External Login Authentication in Asp.net core 2.1
And many more...

Single Sign On using ASP.NET Identity between 2 ASP.NET MVC projects

I have 2 web applications that both share the same main level domain as mentioned below so I can share cookies. Web.conifg in both the projects have same machine key and validation key. Since, I want to use Identity and NOT forms authenticaiton, I do not have an node in either of my web.config file. I am able to create the Auth cookie successully from SSO and can view the authorized pages in SSO but I am still redirected to SSO login when I try to access an authorized view in MVC project.
sso.domain.com - MVC Project
mvc.domain.com - MVC Project
I have a startup.cs file in my SSO and MVC project like below:
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
// For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
public void ConfigureAuth(IAppBuilder app)
{
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Enable the application to use a cookie to store information for the signed in user
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
ExpireTimeSpan = TimeSpan.FromMinutes(3),
LoginPath = new PathString("/Login"),
CookieName = "MyCookieName",
CookieDomain = ".domain.com"
});
app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
}
}
Below is the code I have so far in the SSO project under the AccountController.cs. I call the IdentitySignin function below on validating the user against the database which creates the cookie:
private void IdentitySignin(string userId, string name, string providerKey = null, bool isPersistent = false)
{
var claims = new List<Claim>();
// create *required* claims
claims.Add(new Claim(ClaimTypes.NameIdentifier, userId));
claims.Add(new Claim(ClaimTypes.Name, name));
var identity = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie);
//get the expiry minutes from config or use the default value of 30 minutes
double expiryMinutes;
expiryMinutes = double.TryParse(ConfigurationManager.AppSettings["AuthCookieExpiryMinutes"], out expiryMinutes) ? expiryMinutes : 30;
// add to user here!
AuthenticationManager.SignIn(new AuthenticationProperties()
{
AllowRefresh = true,
IsPersistent = isPersistent,
ExpiresUtc = DateTime.UtcNow.AddMinutes(expiryMinutes),
IssuedUtc = DateTime.UtcNow
}, identity);
}
private void IdentitySignout()
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie, DefaultAuthenticationTypes.ExternalCookie);
}
private IAuthenticationManager AuthenticationManager
{
get
{
return HttpContext.GetOwinContext().Authentication;
}
}
private async Task<string> GetVerifiedUserIdAsync()
{
var result = await AuthenticationManager.AuthenticateAsync(
DefaultAuthenticationTypes.ApplicationCookie);
if (result != null && result.Identity != null
&& !String.IsNullOrEmpty(result.Identity.GetUserId()))
{
return result.Identity.GetUserId();
}
return null;
}
So, I figured out the reason why Single Sign On wasn't working between 2 MVC applications in-spite of both of them sharing the same machine and validation keys.
My SSO MVC application and the other MVC application were both using different version of OWIN and ASP.NET Identity DLLs. I used the Nuget to update the DLLS in one project but did not do the update in the other one.
I hope this helps someone who runs into this problem.
Just as an FYI, to share ASP.NET Identity Authentication between more than 1 application, make sure you have below things in EACH APP:
Same Machine Keys and Validation Keys in the web.config file
Same versions of OWIN and ASP.NET IDENTITY DLLs
SAME COOKIE NAME AND COOKIE DOMAIN in the Startup.cs
Both app needs to be on same domain to share the auth cookie
Most likely, you haven't set a shared machine key. The auth cookie is encrypted, and unless both sites share the same machine key, one cannot decrypt what the other encrypted. Add the following to both project's Web.config:
<sytem.web>
...
<machineKey validation="HMACSHA256" validationKey="[validationKey]" decryptionKey="[decryptionKey]" compatibilityMode="Framework45" />
To generate the keys, in IIS, click on the server in the left pane, and then the Machine Key control panel item. Choose your validation method (above, I've used HMACSHA256). I don't recommend using the default of SHA1 as that's ridiculously easy to crack. Then, on the right "Actions" panel, click "Generate Keys". Copy the two text box values to the appropriate attributes of this config element, and make sure they are the same for all projects that need to share the auth cookie.

Using ASP.NET Role Authorisation with IdentityServer3 implicit flow

My single page application uses OidcTokenManager to connect to an IdentityServer3 STS using implicit flow. The client presents the IDS3 access token to a ASP.NET Core (WebApi) web service as a Bearer Token; the web service application is configured to use IDS3 middleware and restricts access to its methods using an Authorize attribute.
SPA client configuration:
function configureTokenManager() {
console.log("configureTokenManager()");
var config = {
authority: $config.authority,
client_id: "BNRegistry",
redirect_uri: $config.webRoot + "/#/authorised/",
post_logout_redirect_uri: $config.webRoot + "/#/",
response_type: "id_token token",
scope: "openid profile email BNApi",
silent_redirect_uri: $config.webRoot + "/#/renew/",
silent_renew: true,
filter_protocol_claims: false
};
return new OidcTokenManager(config);
};
Scope configuration in STS:
new Scope
{
Name = "BNApi",
DisplayName = "BN Api",
Enabled = true,
Type = ScopeType.Resource,
Claims = new List<ScopeClaim>
{
new ScopeClaim(Constants.ClaimTypes.Name),
new ScopeClaim(Constants.ClaimTypes.Role)
}
}
WebApi configuration:
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = Configuration["Authority"],
RequiredScopes = new[] {"BNApi"},
NameClaimType = IdentityModel.JwtClaimTypes.Name,
RoleClaimType = IdentityModel.JwtClaimTypes.Role
});
WebApi method:
[Authorize]
public IActionResult Get()
{
...
}
This works as expected, rejecting an unauthenticated user with a 401. If I examine the claims for the user in the api controller method, (eg. User.Claims.ToList()), it contains entries for any roles to which the user has been assigned.
However, if I examine the User.Identity.Name property it is always null, and if I query User.IsInRole("Administrator") it is always false, even when the user is assigned to that role. Further, if I add a role name to the Authorize attribute ([Authorize(Role="Administrator")]), users are rejected with a 401 whether or not they belong to the stated role.
How can I get IdentityServer3 to play nicely with ASP.NET Role authorisation?
Have you tried resetting the InboundClaimTypeMap?
From the IdentityServer3 documentation page here:
When you inspect the claims on the about page, you will notice two
things: some claims have odd long type names and there are more claims
than you probably need in your application.
The long claim names come from Microsoft’s JWT handler trying to map
some claim types to .NET's ClaimTypes class types.
Unfortunately this mapping ends up breaking the specific claim names you have defined as name and role, because their names get transformed and no longer map to what you were expecting. This results in the [Authorize(Roles = "")] and User.IsInRole("") not working as expected.
In your API Startup.cs you should add the following:
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions()
{
...
});
EDIT: The below information was incorrect!. As pointed out by #Paul Taylor "the AlwaysInclude property ensures that the relevant claim is always present in an identity token (which is used with the client, not the API). This is a resource scope so the property has no effect.". Thanks for helping me understand a little more about how IdentityServer works :-)
For the Name and Role claims to be included when accessing the API, you need to specifically mark them as alwaysInclude in your ScopeClaim list.
new Scope
{
Name = "BNApi",
DisplayName = "BN Api",
Enabled = true,
Type = ScopeType.Resource,
Claims = new List<ScopeClaim>
{
new ScopeClaim(Constants.ClaimTypes.Name, true), //<-- Add true here
new ScopeClaim(Constants.ClaimTypes.Role, true) // and here!
}
}

Categories

Resources