IdentityServer gets into infinite loop of authentication - thinktecture-ident-server

I have the following client set up in IdentityServer:
new Client
{
ClientName = "My web application",
Enabled = true,
ClientId = "mywebapp",
ClientSecrets = new List<ClientSecret>
{
new ClientSecret("somesecret")
},
Flow = Flows.Hybrid,
ClientUri = "https://app.mydomain.com",
RedirectUris = new List<string>
{
"oob://localhost/wpfclient",
"http://localhost:2672/",
"https://app.mydomain.com"
}
}
And it is hosted online, let's say https://auth.mydomain.com/core.
Trying to modify the MVC OWIN Client (Hybrid) sample client to log-in to the above identity server, in Startup.cs I modified the ClientId, ClientSecret and RedirectUri to match the client settings in IdSrv. Now when I try to navigate to a page that requires authorization, I am redirected to IdentityServer's URL. When I log-in, the breakpoint hits at AuthorizationCodeReceived notification in the client's Startup.cs and then gets into a loop. The browser's status shows:
Waiting for localhost...
Waitnig for auth.mydomain.com...
Waiting for localhost...
Waitnig for auth.mydomain.com...
...
and so on and never finishes the log-in. Why is this happening? Please help.
Thanks!

Most probably this is caused by mixing http and https in redirects. Please use one scheme consistently and check the scheme on browser address-bar.

Related

Windows authentication fail with "401 Unauthorized"

I have a MVC client accessing a Web API protected by IDS4. They all run on my local machine and hosted by IIS. The app works fine when using local identity for authentication. But when I try to use Windows authentication, I keep getting "401 Unauthorized" error from the dev tool and the login box keeps coming back to the browser.
Here is the Windows Authentication IIS setting
and enabled providers
It's almost like that the user ID or password was wrong, but that's nearly impossible because that's the domain user ID and password I use for logging into the system all the time. Besides, according to my reading, Windows Authentication is supposed to be "automatic", which means I will be authenticated silently without a login box in the first place.
Update
I enabled the IIS request tracing and here is the result from the log:
As you can see from the trace log item #29, the authentication (with the user ID I typed in, "DOM\Jack.Backer") was successful. However, some authorization item (#48) failed after that. And here is the detail of the failed item:
What's interesting is that the ErrorCode says that the operation (whatever it is) completed successfully, but still I received a warning with a HttpStatus=401 and a HttpReason=Unauthorized. Apparently, this is what failed my Windows Authentication. But what is this authorization about and how do I fix it?
In case anyone interested - I finally figured this one out. It is because the code that I downloaded from IndentityServer4's quickstart site in late 2020 doesn't have some of the important pieces needed for Windows authentication. Here is what I had to add to the Challenge function of the ExternalController class
and here is the ProcessWindowsLoginAsync function
private async Task<IActionResult> ProcessWindowsLoginAsync(string returnUrl)
{
var result = await HttpContext.AuthenticateAsync(AccountOptions.WindowsAuthenticationSchemeName);
if (result?.Principal is WindowsPrincipal wp)
{
var props = new AuthenticationProperties()
{
RedirectUri = Url.Action(nameof(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));
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(IdentityConstants.ExternalScheme, new ClaimsPrincipal(id), props);
return Redirect(props.RedirectUri);
}
else
{
return Challenge(AccountOptions.WindowsAuthenticationSchemeName);
}
}
Now my windows authentication works with no issues.

Not able to SignOut using Saml2 from Sustainsys

This should be redirecting my app to my AdFs signOut Page, and then redirect me back to my app.
However, it simply redirects me to my route "/logout".
Watching the log on my ADFS server nothing happens.
[AllowAnonymous]
[HttpGet]
[Route("api/logout")]
public async Task<IActionResult> Logout()
{
return SignOut(new AuthenticationProperties()
{
RedirectUri = "/logout"
},
Saml2Defaults.Scheme);
}
SignIn works fine. I even tried this same approach, but does not work. Here, the ReturnUrl method gets the location from HttpContext.Response.Header. When I try this for the logout, the location is always null.
[AllowAnonymous]
[HttpGet]
[Route("api/login")]
public async Task<string> LoginAdfs()
{
string redirectUri = _appSettings.Saml.SpEntityId;
await HttpContext.ChallengeAsync(new AuthenticationProperties
{
RedirectUri = string.Concat(redirectUri, "/autenticado")
});
return ReturnUrl();
}
Any idea what could be happening?
UPDATE 21/11/2019
Turns out the Saml2Handler is simply not trying to send the request to the server. I'm getting these messages on my output window:
Sustainsys.Saml2.AspNetCore2.Saml2Handler: Debug: Initiating logout, checking requirements for federated logout
Issuer of LogoutNameIdentifier claim (should be Idp entity id):
Issuer is a known Idp: False
Session index claim (should have a value):
Idp has SingleLogoutServiceUrl:
There is a signingCertificate in SPOptions: True
Idp configured to DisableOutboundLogoutRequests (should be false):
Sustainsys.Saml2.AspNetCore2.Saml2Handler: Information: Federated logout not possible, redirecting to post-logout
Here is my StartUp Configuration, I don't get what is wrong here:
ServiceCertificate se = new ServiceCertificate()
{
Certificate = new X509Certificate2(SpCert, "",X509KeyStorageFlags.MachineKeySet),
Use = CertificateUse.Signing
};
SPOptions sp = new SPOptions
{
AuthenticateRequestSigningBehavior = SigningBehavior.Never,
EntityId = new EntityId(SpEntityId),
ReturnUrl = new Uri("/login"),
NameIdPolicy = new Sustainsys.Saml2.Saml2P.Saml2NameIdPolicy(null, Sustainsys.Saml2.Saml2P.NameIdFormat.Unspecified),
};
sp.ServiceCertificates.Add(se);
IdentityProvider idp = new IdentityProvider(new EntityId(appSettings.Saml.EntityId), sp);
idp.Binding = Saml2BindingType.HttpPost;
idp.AllowUnsolicitedAuthnResponse = true;
//idp.WantAuthnRequestsSigned = true;
idp.SingleSignOnServiceUrl = new Uri("/login");
//idp.LoadMetadata = true;
idp.SigningKeys.AddConfiguredKey(new X509Certificate2(IdpCert));
idp.MetadataLocation = theMetadata;
idp.DisableOutboundLogoutRequests = true;
For the logout to work, two special claims "LogoutNameIdentifier" and "SessionIndex" (full names are http://Sustainsys.se/Saml2/LogoutNameIdentifier and http://Sustainsys.se/Saml2/SessionIndex need to be present on the user. Those carries information about the current session that the Saml2 library needs to be able to do a logout.
Now I don't see your entire Startup, so I cannot understand your application's flow. But those claims should be present in the identity returned by the library - possibly stored in an External cookie (if you are using asp.net identity). When your application then sets the application cookie those two claims must be carried over to the session identity.
Also you have actually disabled outbound logout with DisableOutboundLogoutRequests. But that's not the main problem here as your logs indicates that the required claims are not present.
From my own experience, the two claims, as mentioned by Anders Abel, should be present on the user. I had not seen these claims until I passed all of the claims along with the sign-in request. ASP.NET Core recreates the principal on SignInAsync and needs claims to be passed in with the request.
With the following, I am able to fulfill a SingleLogout with my service:
await HttpContext.SignInAsync(user.SubjectId, user.Username, props, user.Claims.ToArray());
what you are using as a service provider.

ASP.net Core 2.1 and IdentityServer4 - Client Side and Server Side Cookie Removal

I am working on an Identity Server implementation that makes use of ASP.net Core 2.1 and IdentityServer4 libraries. In the context of OAuth2 protocol, the identity server is implemented in a way to return an AuthorizationCode as soon as the customer provides his/her login credentials through a server provided web-form. The code is returned by the server to a redirectURI that the customer has provided earlier when he first made the login request (see below shown sample login request).
1) EXAMPLE SCENARIO
Sample Login Request:
http://exampleABC.com:5002/connect/authorize?client_id=XYZ&scope=myscope&response_type=code&redirect_uri=http://exampleXYZ.com
Once above like request is issued in browser, the browser opens up a client login page where user is asked to type in his customerid and password. Then, an SMS token page is opened where the customer enters the SMS he has received at his cell phone. The customer then enters the SMS in the browser. Finally, the server redirects the customer's browser to the page at the redirectURI where the browser shows the AuthorizationCode (i.e. code) in the address bar as shown in the following:
https://exampleXYZ.com/?code=89c0cbe1a2cb27c7cd8025b6cc17f6c7cf9bc0d4583c5a63&scope=myscope
Here, the code "89c0cbe1a2cb27c7cd8025b6cc17f6c7cf9bc0d4583c5a63" can be now used to request an AccessToken from the identity server.
2) PROBLEM STATEMENT
If I re-issue the above indicated sample login request in the same client browser (e.g. chrome), then the browser redirects the user to the redirectURI immediately without re-asking the client login credentials. This is a problem because I have to open up a fresh login screen every time the login request is made considering that there can be customers who have different login credentials. Therefore, I have provided a logout endpoint in my IdentityServer implementation where I intend to clean out the entire client cache and then sign out the customer as shown in the following code block. Here, I delete the cookies first and then create a new one with same key and past expiration date in order that the cookie is removed from the client browser cache in addition to the server cache. My aim here is to bring the login web form up-front in the browser at all times with no caching in place if a logout request is issued in order that the login form is displayed every time a new comer customer arrives.
public async Task<IActionResult> Logout()
{
var vm = await BuildLoggedOutView();
string url = Url.Action("Logout", new { logoutId = vm.LogoutId });
try
{
if (HttpContext.Request != null && HttpContext.Request.Cookies != null && HttpContext.Request.Cookies.Keys != null && HttpContext.Request.Cookies.Keys.Count > 0)
{
foreach (var key in _accessor.HttpContext.Request.Cookies.Keys)
{
//!!!! Cookie Removal !!!!!!
//Here I delete the cookie first and then recreate it
//with an expiry date having the day before.
_accessor.HttpContext.Response.Cookies.Delete(key);
_accessor.HttpContext.Response.Cookies.Append(
key,
string.Empty,
new CookieOptions()
{
Expires = DateTime.Now.AddDays(-1)
});
}
}
//!!!! Explicit sign out!!!!!!
await _accessor.HttpContext.SignOutAsync();
}
catch (NotSupportedException ex) // this is for the external providers that don't have signout
{
}
catch (InvalidOperationException ex) // this is for Windows/Negotiate
{
}
return View("Logged out", vm);
}
3) QUESTION:
Although I delete the cookies and override them on server side, the client browser keeps returning into the page at redirect uri where a new authorization code is shown without enforcing the customer to login (which is undesired). So, my question here is what am I missing in the above code block? It looks neither cookie override with old expiry date nor the explicit call to SignoutAsync method does not help to sign out the customer completely. Is there some more explicit strategy you might suggest in order to clean out everything both on client and server side completely once logged out?
I've had the same issue with cookies not being deleted properly. In my case it was because I defined a specific path for the authentication cookies. Let's say my path was /path, in that case you have to specify the same path within your delete:
foreach (var cookie in Request.Cookies.Keys)
{
Response.Cookies.Delete(cookie, new CookieOptions()
{
Path = "/path",
// I also added these options, just to be sure it matched my existing cookies
Expires = DateTimeOffset.Now,
Secure = true,
SameSite = SameSiteMode.None,
HttpOnly = true
});
}
Also, I do not know if the .Append() is necessary. By using .Delete() it already sent a set-cookie header in my case.

Bookmarking login page with nonce

I'm trying to integrate an MVC4 web client with IdentityServer using Microsoft OWIN middleware OIDC authentication v4.0.0. When requesting an ID token from the authorize endpoint, a nonce must be supplied, and the login page served up has the nonce in the query string. If a user bookmarks this and uses it to log in the next day (for example), nonce validation in the client will fail because they'll no longer have that nonce stored, or it will have expired, etc.
This triggers the AuthenticationFailed notification in the client with this exception:
"IDX21323: RequireNonce is '[PII is hidden]'. OpenIdConnectProtocolValidationContext.Nonce was null, OpenIdConnectProtocol.ValidatedIdToken.Payload.Nonce was not null. The nonce cannot be validated. If you don't need to check the nonce, set OpenIdConnectProtocolValidator.RequireNonce to 'false'. Note if a 'nonce' is found it will be evaluated."
At this point I could HandleResponse, redirect to an error page and so on. If they then try to access a protected resource again, the redirect to IdentityServer immediately returns an ID token due to the previous successful login (from its point of view I guess?) and this time the nonce validates and the user is logged into the client. But this is a rather strange experience for the user - their first attempt to log in appears to fail, they get an error, but then when they try again they don't even have to log in, they're just taken straight in.
An alternative would be to handle this type of exception in AuthenticationFailed by redirecting to the home protected resource so the happens 'seamlessly' in the background. To the user it appears as if their first login attempt worked. But I'm not sure if this is appropriate for genuine nonce validation issues. I'm also worried this may lead to redirect loops in some cases.
So to get to my question... what is the common approach to this issue of bookmarking login pages / nonces? Have I made a fundamental mistake or picked up a fundamental misunderstanding somewhere along the line which has allowed this scenario to occur?
Here is the code that needs to go into the call to
UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "oidc",
Authority = appSettings["ida:Authority"],
ClientId = appSettings["ida:ClientId"],
ClientSecret = appSettings["ida:ClientSecret"],
PostLogoutRedirectUri = appSettings["ida:PostLogoutRedirectUri"],
RedirectUri = appSettings["ida:RedirectUri"],
RequireHttpsMetadata = false,
ResponseType = "code id_token",
Scope = appSettings["ida:Scope"],
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = authFailed =>
{
if (authFailed.Exception.Message.Contains("IDX21323"))
{
authFailed.HandleResponse();
authFailed.OwinContext.Authentication.Challenge();
}
return Task.FromResult(true);
}
},
SignInAsAuthenticationType = "Cookies"
}
});

thinktecture identity server 3 authentication works correctly in iis express, but keeps on throwing 401 unatuhorized when hosted in iis

Ok so i tried hosting the simplest oauth sample and the identity server both on iis, i have enable cors on the simplest oauth sample. So when i test the api using the javascript implicit client, on iis express it works flawlessly, it gets the token then when the token is sent the web api checks the token and authorizes the javascript client. the problem happens when i move the javascript imlicit client, the identity server, and the simple oath web api is hosted on iis, the javascript brings back the token correctly but when the token is sent to the web api it always return 401 unauthorized. So is there any configuration i have to add in order to run it on iis. i have made sure that anonymous authentication is the only enab;ed authentication mode. Any help or pointer is deeply appreciate.
I am trying to implement the samples given on iis. thanks for the help
I had the same issue. It was coming from my self signed certificate.
Try adding to your IdentityServerOptions
RequireSsl = false
and switch the WebApi Authority to use http.
Edit
Server Side Configuration
public void ConfigureIdentityServer(IAppBuilder app)
{
//Configure logging
LogProvider.SetCurrentLogProvider(new DiagnosticsTraceLogProvider());
//This is using a Factory Class that generates the client, user & scopes. Can be seen using the exmaples
var IdentityFactory = Factory.Configure("DefaultConnection");
app.Map("/identity", idsrvApp =>
{
idsrvApp.UseIdentityServer(new IdentityServerOptions
{
SiteName = "Security Proof of Concept",
SigningCertificate = LoadCertificate(),
Factory = IdentityFactory,
CorsPolicy = CorsPolicy.AllowAll,
RequireSsl = false
});
});
}
JavaScript
After receiving the token make sure it's inserted in the Authorization Header..
JQuery Example
$.ajax({
url: 'http://your.url',
type: GET,
beforeSend: function (xhr) {
xhr.withCredentials = true;
xhr.setRequestHeader("Authorization", " Bearer " + apiToken);
}
});
WebApi Resource
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
//Location of identity server make full url & port
Authority = "http://localhost/identity",
RequiredScopes = new[] { "WebApiResource" }
//Determines if the Api Pings the Identity Server for validation or will decrypt token by it's self
//ValidationMode = ValidationMode.Local
});
Best way to determine what is happening is enable logging.