Cannot Signout the External Identity provider in IdentityServer - asp.net-mvc

I've an MVC Application which uses IdentityServer4. In IdentityServer4, I registered SAML2 (SustainSys.SAML2) as the external Login provider. and Login works fine.
When user log out of the MVC application, it logs out from the MVC application but the log out for External Login Provider isn't triggering. I checked the LogOut method of my identity Server which does the redirect to External Authentication Scheme. but the redirect doesnt happen.
this triggers a redirect to the external provider for sign-out
return SignOut(new AuthenticationProperties { RedirectUri = url },
vm.ExternalAuthenticationScheme);
And here is the code where in i registered External Identity Provider for SAML. I've used Nuget package from SustainSys SAML.
.AddSaml2(options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.SPOptions = CreateSPOptions();
var idp = new IdentityProvider(new EntityId(_strIDPEntityId), options.SPOptions)
{
AllowUnsolicitedAuthnResponse = true,
Binding = Saml2BindingType.HttpRedirect,
SingleSignOnServiceUrl = new Uri(_strSingleSignOnURL),
SingleLogoutServiceBinding = Saml2BindingType.HttpRedirect,
SingleLogoutServiceUrl = new Uri("https://devit-dev.onelogin.com/trust/saml2/http-redirect/slo/1111")
};
idp.SigningKeys.AddConfiguredKey(
new X509Certificate2(
AppDomain.CurrentDomain.SetupInformation.ApplicationBase + "../../../App_Data/OneLogin.cer"));
options.IdentityProviders.Add(idp);
});
Not sure what am i missing here. Appreciate any help.

Check your logs, it should show you the decision process that ends up in a local logout. There are A LOT of things that need to be in place for a federated logout to work. You need a service certificate and you need some special claims. The latter will be simplified in a future compatibility release with Sustainsys.Saml2/IdSrv4

Related

How to make secure authentication for .NET Core Web API?

I am developing an app with .NET Core Web API, Entity Framework and React. I've been reading a lot recently about possible authentication techniques for my API and I've discovered that plain JWT is not entirely secure, so at first I decided to use OpenID Connect with IdentityServer 4. I understand the idea behind OAuth 2.0 and OpenID Connect is to hide user credentials during login process and to involve external authentication provider in issuing an access token, but I don't want to rely on such services because not everyone have an account on Facebook etc. I consider this as an optional way to login. I want to give users an ability to sign in with just login and password. So what is the best (secure) way to accomplish this in modern web apps?
Having project 1 as Client App, project 2 as API Resources and project 3 as Authorization Service (IdentityServer4), I consider following scenarios:
A user is able to create an account on Authorization Service which is responsible for issuing a token required to get access to API Resources through Client App. Authorization Service is registered as authorization provider only for my Client App.
Get authorization token from Authorization Service using resource owner password grant - this one is not recommended by the specs but in my case since user must provide credentials to Authorization Service anyway and I will be hosting every project I can't see any problem.
Don't bother with OAuth and implement authorization mechanism using ASP.NET Core Identity + bearer token authentication.
Any ideas or recommendations highly apprecieated.
I use the JwtBearer package, wire it up in your Startup.cs Configure method like
.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
TokenValidationParameters = new TokenValidationParameters
{
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Configuration["AppSettings:AuthConfig:SecretKey"])),
ValidateIssuer = true,
ValidIssuer = Configuration["AppSettings:AuthConfig:Issuer"],
ValidateAudience = true,
ValidAudience = Configuration["AppSettings:AuthConfig:Audience"],
ValidateLifetime = true,
}
})
and my login action on my User controller looks like
[HttpPost]
public string Post([FromBody]LoginRequest request)
{
var contact = dbContext.Contacts.Where(c => c.Active && c.Email == request.Email).Select(c => new { c.Id, c.PasswordHash }).SingleOrDefault();
if (contact == null || !Security.PasswordHash.ValidatePassword(request.Password, contact.PasswordHash))
{
return string.Empty;
}
var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(appSettings.AuthConfig.SecretKey));
var now = DateTime.UtcNow;
var claims = new Claim[]
{
new Claim(JwtRegisteredClaimNames.Sub, contact.Id.ToString()),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.Now.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64)
};
var jwt = new JwtSecurityToken(
issuer: appSettings.AuthConfig.Issuer,
audience: appSettings.AuthConfig.Audience,
claims: claims,
notBefore: now,
expires: now.AddDays(30),
signingCredentials: new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256));
jwt.Payload.Add("roles", dbContext.ContactRoles.Where(cr => cr.ContactId == contact.Id).Select(ur => ur.Role.Name).ToArray());
return new JwtSecurityTokenHandler().WriteToken(jwt);
}
I use a JWT package for Angular on the client, there may be something similar for React.

How does ADFS keep a user authenticated?

I'm using WsFed to implement ADFS SSO into an app. If I try to run [Authorize] methods, I'm taken to the sign in page. When I sign in, a cookie with encrypted information is created and I'm able to run [Authorize] methods. The cookie has option ExpireTimeSpan = TimeSpan.FromSeconds(10);. So far, this works as expected and an unauthorized user cannot access the app.
The confusion begins when the cookie expires, is altered, or deleted from the browser. When this happens, if I run an [Authorized] method I'm automatically signed in again without needing to reenter my credentials and the cookie is recreated. However, if I explicitly sign out using return SignOut(... method, then I am required to reenter my credentials.
Why does ADFS re-authenticate me if I delete the cookie, and how does it know to do so? It doesn't do it if I explicitly sign out. Shouldn't remaining authenticated depend on the cookie being present with the correct values?
Authentication setup in Startup.ConfigureServices:
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme;
})
.AddWsFederation(options =>
{
options.Wtrealm = Configuration["AppSettings:wsfed:realm"];
options.MetadataAddress = Configuration["AppSettings:wsfed:metadata"];
options.UseTokenLifetime = false;
})
.AddCookie(options =>
{
options.Cookie.Name = "AuthenticationCookie";
options.LoginPath = "/signin-wsfed";
options.LogoutPath = "/NameController/Logout";
options.ExpireTimeSpan = TimeSpan.FromSeconds(10);
options.SlidingExpiration = true;
});
Login action:
[AllowAnonymous]
[HttpGet]
public IActionResult Login()
{
var authProperties = new AuthenticationProperties
{
RedirectUri = "https://app:1234/NameController/Index",
};
return Challenge(authProperties, WsFederationDefaults.AuthenticationScheme);
}
Logout action:
[AllowAnonymous]
[HttpGet]
public IActionResult SignOutOfADFS()
{
return SignOut(
new AuthenticationProperties
{
RedirectUri = "https://app:1234/NameController/AfterLogout"
},
CookieAuthenticationDefaults.AuthenticationScheme,
WsFederationDefaults.AuthenticationScheme);
}
The AD FS is an identity provider that is commonly used for single sign-on purposes. As part of that, a key feature is that the AD FS does remember the signed-in user in order to authenticate them for another website. It does that by remembering the user using a separate session persisted using a cookie for the AD FS website.
When you sign out locally from your application, then all you are doing is clearing your local cookie. So when you try to authenticate again and the user is challenged to authenticate with the identity provider, the AD FS is able to sign the user in without asking them again for their credentials. For the AD FS your application is then just like a third website which is asking for authentication after the user already signed in into the AD FS.
In order to sign out completely, you will have to do a WSFederation sign out. As part of that process, the local cookie is cleared and then the user is redirected to an AD FS signout page where the AD FS authentication cookie is also cleared. On a subsequent authentication attempt, the AD FS then cannot remember the user anymore (since there’s no cookie) so they have to authenticate again with their credentials. That is what you are doing in your SignOutOfADFS action.
The WSFederation protocol supports a way for the authenticating application to require the user to reauthenticate with the identity provider by passing the wfresh=0 parameter with the authentication request. This is also supported in current AD FS versions. Unfortunately, I don’t think this parameter is currently supported by the WSFederation authentication handler for ASP.NET Core. It wouldn’t really prevent the user from reusing their authentication though, so you wouldn’t be able to use this a security feature.

Identity Server 4 with EF identity DB - OpenID Connect Failing

PLEASE NOTE: This issue was not resolved in this post. I was asked to create a new post. Please refer to the new post titled:
Identity Server 4 with EF identity DB - OpenID Connect Failing (1)
I have an Identity Server 4 solution with EF Identity DB. I can login with my email and external gmail account, but when I try to login using OpenID (User name and Password) I receive the error below. The issue maybe with the info stored in the Identity DB tables. I'm new to Identity Server and this is my first attempt working with EF Identity DB. I can post DB info if it helps resolve the issue.
Source code:
https://github.com/gotnetdude/GotNetDude-PublicRepository/tree/master/AuthServer
Identity Server Log File:
https://github.com/gotnetdude/GotNetDude-PublicRepository/blob/master/AuthServer_log.txt
MVC Client Log:
https://github.com/gotnetdude/GotNetDude-PublicRepository/blob/master/MVCClient_log.txt
Any suggestions would be appreciated. Paul
EDIT
Please find below the Client configuration info from the identity tables. I'm not sure where to set the AllowedRedirectUris in the DB. The other question I have is why does it work when I sign-in with my email account?
Here is the AuthServer Startup code where I add oidc mvc client as the challenge option ("OpenID Connect") which is fail. The MVC client works fine if I login with the email credentials. I guess is that this has some to do with the way the scope is being handled on the mvc client. Any suggestion are appreciated.
services.AddAuthentication()
.AddGoogle("Google", options =>
{
options.ClientId = "434483408261-55tc8n0cs4ff1fe21ea8df2o443v2iuc.apps.googleusercontent.com";
options.ClientSecret = "3gcoTrEDPPJ0ukn_aYYT6PWo";
})
.AddOpenIdConnect("oidc", "OpenID Connect", options =>
{
//options.Authority = "https://demo.identityserver.io/";
//options.ClientId = "implicit";
//options.SaveTokens = true;
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.SaveTokens = true;
options.ClientId = "mvc";
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
I'm trying to connect to authority (:5000) from the MVC Client (:5002) see image below:
Debug results from AccountService:
There are 3 instances IdentityServer4.EntityFramework.Entities.Client in the client store under context, clients. All 3 instances had there EnableLocalLogin set to True. I hit the break point after selecting OpenID Connect option.
I also set a breakpoint at the top of the login controller, which it never reached:
I think the problem is in line 74 from AuthServer/Startup:
.AddOpenIdConnect("oidc", "OpenID Connect", options =>
{
...
options.ClientId = "mvc";
...
});
The server is not the mvc client. I think this is 'confusing' IdentityServer. You don't need to add oidc to the server. If you remove theses lines then it should work.
If you login from the mvc client website (:5002) then you should be redirected. And if you login to IdentityServer (:5000) you don't have to be redirected. The server is the authority, the resource is identified by a scope and the client by a clientid.
Check the Log files that you have provided. The error that you are receiving is
Invalid redirect_uri: http://localhost:5000/signin-oidc.
If you check your client configuration your AllowedRedirectUris contains http://localhost:5002/signin-oidc.
You have a (typo) mistake in the port. It must be 5002.
EDIT
According to your screenshots, and to the log files, your client is properly configured on the Identity Server side. The problem is in your MVC client, not in the database. You need to look there, and find what RedirectUrl you are setting when starting the client itself.
EDIT 2:
OK, after looking at your code I realized that what #Ruard van Elburg is telling you is the reason for having this problem. When using the Internal authentication, you don't need to specify it like this (you are really confusing Identity Server). This specification is for external Oidc provider only (for example Okta, or whatever else Oidc provider you have). Check here. You see - the Identity Server Startup.cs doesn't contain this code that you have (line 74 to 89 here). Why don't we do this step by step. Try with removing the lines that I mentioned.

ASP.net Identity stops handing out External Cookie after removing external account

I have a site setup with four 3rd-party login services, Microsoft, VS, Github, and Linkedin. Everything seems to work great, I can log in/out, add/remove external accounts with no problem.
Randomly however, it seems to stop working. When I try to login using any of the 3rd-party services, it just kicks me back to the login page.
Looking at the ExternalLoginCallback it appears that the AuthenticateResult.Identity is null and it can't get the external login info. Looking at it on the client-side it looks like they never got the external signin cookie.
I still can't consistently reproduce this error, so it's really hard to determine what might be happening. Any help would be great.
Update 1: I was able to identify the steps to reproduce:
Login to an account with more than 1 associated login
Remove one of the logins
In a new browser or a private session, try to log in with any of the 3rd-party accounts and you will be returned to login without an external cookie.
After hitting the error it won't hand out a cookie to any new sessions until IIS is restarted.
Update 2: Looks like it has something to do with setting a Session variable.
On the removeLogin action I was adding a value to the session. I'm not sure why but when I stopped doing that, I stopped having my problem. Time to figure out why... Update 3: Looks like this problem has been reported to the Katana Team
Update 4: Looks like someone else already ran into this problem. Stackoverflow post. They didn't give all of the code you needed to solve it, so I'll include that here as an answer.
Startup.Auth.cs
public void ConfigureAuth(IAppBuilder app) {
// Configure the db context, user manager and signin manager to use a single instance per request
app.CreatePerOwinContext(appContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
// 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
// Configure the sign in cookie
app.UseCookieAuthentication(new CookieAuthenticationOptions {
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
AuthenticationMode = AuthenticationMode.Active,
LoginPath = new PathString("/Login"),
LogoutPath = new PathString("/Logout"),
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 = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, User, int>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentityCallback: (manager, user) => user.GenerateUserIdentityAsync(manager),
getUserIdCallback: (id) => (Int32.Parse(id.GetUserId()))
)
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process.
app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
// Enables the application to remember the second login verification factor such as phone or email.
// Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from.
// This is similar to the RememberMe option when you log in.
app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
// Uncomment the following lines to enable logging in with third party login providers
app.UseMicrosoftAccountAuthentication(new MicrosoftAccountAuthenticationOptions{
ClientId = ConfigurationManager.AppSettings["MSA:Id"],
ClientSecret = ConfigurationManager.AppSettings["MSA:Secret"],
Caption = "Microsoft"
});
app.UseVisualStudioAuthentication(new VisualStudioAuthenticationOptions(){
AppId = ConfigurationManager.AppSettings["VSO:Id"],
AppSecret = ConfigurationManager.AppSettings["VSO:Secret"],
Provider = new VisualStudioAuthenticationProvider(){
OnAuthenticated = (context) =>{
context.Identity.AddClaim(new Claim("urn:vso:access_token", context.AccessToken, XmlSchemaString, "VisualStudio"));
context.Identity.AddClaim(new Claim("urn:vso:refresh_token", context.RefreshToken, XmlSchemaString, "VisualStudio"));
return Task.FromResult(0);
}
},
Caption = "Visual Studio"
});
app.UseGitHubAuthentication(new GitHubAuthenticationOptions{
ClientId = ConfigurationManager.AppSettings["GH:Id"],
ClientSecret = ConfigurationManager.AppSettings["GH:Secret"],
Caption = "Github"
});
app.UseLinkedInAuthentication(new LinkedInAuthenticationOptions {
ClientId = ConfigurationManager.AppSettings["LI:Id"],
ClientSecret = ConfigurationManager.AppSettings["LI:Secret"],
Caption = "LinkedIn"
});
}
OWIN and asp.net handle cookies/session differently. If you authorize with OWIN before you initialize a session, anyone after a session is initialized will not be able to login.
Workaround: Add the following to your Global.asax
// Fix for OWIN session bug
protected void Application_AcquireRequestState() {
Session["Workaround"] = 0;
}
}
Long term: The way OWIN and asp.net handle sessions/cookies will be merged in vNext, use the work around until then...

External Cookie for External Login in ASP.NET OWIN

We have a legacy system which is built on ASP.NET Mvc 4, now we would like to support Signal Sign On via Azure Active Directory for current users as well as new users. Since we have managed our own authentication workflow, ASP.NET Identity definitely does not fit in our case.
I have managed to build a demo which is working on OWIN OpenIdConnect middleware passive mode without using ASP.NET Identity. The below code works correctly:
app.SetDefaultSignInAsAuthenticationType("ExternalCookie");
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "ExternalCookie",
AuthenticationMode = AuthenticationMode.Passive,
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Passive,
ClientId = ClientId,
Authority = Authority
// More code
});
And in ExternalLoginCallback action:
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
var authManager = Request.GetOwinContext().Authentication;
var result = await authManager.AuthenticateAsync("ExternalCookie");
authManager.SignOut("ExternalCookie");
//More code to convert to local identity
}
This case is really common even using other providers like Google, Facebook or Twitter. One thing I have not much clear is ExternalCookie, maybe I have missed the whole thing. My understanding is when external login is successfully, external cookie is used to store the external claim identity. And then we call:
var result = await authManager.AuthenticateAsync("ExternalCookie");
authManager.SignOut("ExternalCookie");
In order to get the external claim identity and then convert external identity to local identity. I have a little bit confusion why we have to call SignOut external cookie in this case.
Also, I'm not sure whether External Cookie is a must when using external login, or do we have other ways around without using External Cookie.
Please someone give an explanation on this point.
To answer your last question, you change the name of cookie in startup.auth file where you configure external cookie -
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
You can use a string instead of DefaultAuthenticationTypes enum and directly specify the name of the cookie like -
app.UseExternalSignInCookie("myExternalCookie");

Resources