How to achieve Single Logout using Thinktecture Identity Server3 and OpenId owin middleware for multiple ASP.NET MVC applications - identityserver3

I have 2 ASP.NET MVC applications and I am using the OpenID middleware UseOpenIdConnectAuthentication to do the single sign on to both. Configuration is described as below. SSO works great. I login from application 1 and then I check if I am logged into application 2 too that connects to idserver, I can see myself logged in. Both the client applications now have the same id_token and token.
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "oidc",
SignInAsAuthenticationType = "Cookie",
Authority = "<my-idserver-url>",
ClientId = "client1",
RedirectUri = "https://localhost:44360",
PostLogoutRedirectUri = "https://localhost:44360",
ResponseType = "id_token token",
//scopes from config file
Scope = ConfigurationManager.AppSettings.Get("Scope"),
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = n =>
{
var claims = n.AuthenticationTicket.Identity.Claims;
var accepted_claims = new[]{"name", "tenant", "email", "sub"};
var new_claims = claims.Where(x => accepted_claims.Contains(x.Type)).ToList();
new_claims.Add(new Claim("id_token", n.ProtocolMessage.IdToken));
new_claims.Add(new Claim("access_token", n.ProtocolMessage.AccessToken));
var ci = new ClaimsIdentity(new_claims, n.AuthenticationTicket.Identity.AuthenticationType, "email", "role");
n.AuthenticationTicket = new AuthenticationTicket(ci, n.AuthenticationTicket.Properties);
return Task.FromResult(0);
}
On clicking logout in the app, when the RedirectToIdentityProvider method is invoked in the clients, I am setting the id_token as the IdTokenHint as below:
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == Microsoft.IdentityModel.Protocols.OpenIdConnectRequestType.LogoutRequest)
{
var user = n.OwinContext.Authentication.User;
if (user != null && user.Identity.IsAuthenticated)
{
var id_token_claim = user.Claims.FirstOrDefault(x => x.Type == "id_token");
if (id_token_claim != null)
{
n.ProtocolMessage.IdTokenHint = id_token_claim.Value;
}
}
}
return Task.FromResult(0);
}
Problem area > Single Logout:
Success Scenario:
When I logout from the application 1, it redirects to idserver and logs me out and redirects me back to application 1. On debugging I can see that the signout message in the logout method of the DefaultViewService is not null in this case. This works great.
Failure Scenario:
The problem is when I try to logout from application 2 instead of application 1(remember that I logged in from application 1), it does take me to the logout page at idserver, and I can logout. But it does not redirect back to the application 2 and stays on the idserver logged out page. On debugging I can see that the signout message in the logout method of the DefaultViewService is null in this case.
Is it probably because the id_token was originally issued to the application1?
What am I missing?
Do I need to handle this kind of scenario and somehow get hold of the client in identity server, that the user used, to actually logout(application 2)? and then probably issue a new SignoutMessage and put in the postlogout redirect Url for that client?
Or I got it all wrong and this should work automatically?
Update:
A blunder was to try this out with everything on IISExpress localhost on different ports, both clients(locahost, different ports) somehow get the same id_token and access token which shouldn't happen.
Moving out to the IIS hosting different sites solved this and now the applications can get their own individual id_token and access token.
I am still working on the Single Logout, exploring the iFrame approach.

I've been struggling on this myself for a few days. Googling pointed me to your question :)
This is what I've done which worked for me (Not sure if this is the correct approach):
Added both Client Uris in the PostLogoutRedirectUris for each client configuration:
PostLogoutRedirectUris = new List<string>()
{
"http://localhost:28560/",
"http://localhost:57311/"
}

Related

how do I redirect to my own custom logout page after the logout process has completed with .net core 5

Whenever I logout from my app Im taken to a 'default' logged out page (I believe this controller action is built in to .net core). How do I perform a logout redirect to a custom page within my app after Ive been logged out ? I cant see a way to specifiy this, surely it must be possible ? (it was in older versions of .net).
Im using azure ad and my app is hosted in azure. This is the configuration
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
// Handling SameSite cookie according to https://learn.microsoft.com/en-us/aspnet/core/security/samesite?view=aspnetcore-3.1
options.HandleSameSiteCookieCompatibility();
});
// Sign-in users with the Microsoft identity platform
services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "AzureAd");
I also have these settings in appsettings.json
"CallbackPath": "/signin-oidc",
"SignedOutCallbackPath ": "/signout-callback-oidc"
Ive also tried this
new RewriteOptions().Add(
context => {
if (context.HttpContext.Request.Path == "/AzureAD/Account/SignedOut")
{ context.HttpContext.Response.Redirect("/Home/Index"); }
})
);
and even tried adding my own AccountController with these 2 methods
[HttpGet]
public override SignOutResult SignOut()
{
var callbackUrl = Url.Action(nameof(SignedOut), "Account", values: null, protocol: Request.Scheme);
return SignOut(
new AuthenticationProperties { RedirectUri = callbackUrl },
CookieAuthenticationDefaults.AuthenticationScheme,
OpenIdConnectDefaults.AuthenticationScheme);
}
[HttpGet]
public IActionResult SignedOut()
{
if (User.Identity.IsAuthenticated)
{
// Redirect to home page if the user is authenticated.
return RedirectToAction(nameof(HomeController.Index), "Home");
}
return RedirectToAction(nameof(HomeController.Index), "Home");
}
Nothing works, Im always redirected to the 'default' logged out page. Can anyone help ? thanks in advance
I managed to solve this, I changd my Rewite code to this
app.UseRewriter(
new RewriteOptions().Add(
context => {
if (context.HttpContext.Request.Path == "/MicrosoftIdentity/Account/SignedOut")
{ context.HttpContext.Response.Redirect("/Home/SignedOut"); }
})
);
this was the part I had to modify
from
"/AzureAD/Account/SignedOut"
to
"/MicrosoftIdentity/Account/SignedOut"
now it works

ITfoxtec.Identity.Saml2 - Multiple authentication schemes with Asp.net Core Identity

I am aware of the answer given here which is about using Forms Authentication & SAML. In My case I am using Asp.net core Identity on .Net 5. Also I am using two authentication schemes (Cookies & JWT).
My auth pipeline goes as;
//include identity
services.AddIdentity<ApplicationUser, ApplicationRole>(SetupIdentityOptions)
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
//configure cookie and Jwt scheme
services.ConfigureApplicationCookie(...)
services.AddAuthentication(...) //configures default Identity
.AddJwtBearer(options => {...})
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
What I want to know is where should I add SAML2 in this pipeline.
In general the application should;
Be able to login with Cookie authentication (Identity takes care of this)
Jwt tokens should work as well for Apis (this also works with Jwt scheme)
SSO clients should get authenticated from their IdP and when redirected to AssertionConsumer() I will create additional claims create new ClaimsIdentity, create a JWT token(pass it to client) and get the user to dashboard.
I am stuck at 3rd point which is to properly add the SAML scheme without overriding cookie/jwt schemes.
The error No sign-in authentication handler is registered for the scheme 'saml2' probably occurs bedause you need to add services.AddSaml2() and app.UseSaml2()
You can use the setup from the provided example project. Newly added middleware should not interfere with what you already have.
When a SAML2 IdP redirects back to your application, you are given a result that identifies the authenticated user, e.g. Email Address or SSN (in case it is a government Id Provider).
You can combine that information with a Role (e.g. SpecialCustomer, Citizen, or an existing Role that you already have) into a cookie or JWT Token as you probably already do for other users. This can always be distinguished from other cookies and tokens by the Role. Regular [Authorize(....)] attributes will also work just fine.
I was stuck at the same point.. The solution I found:
If you check the source code of IFOXTEC.IDENTITY.SAML2, the method AddSaml2 overrides your AddAuthentication method and adds the AddCookie section.
public static IServiceCollection AddSaml2(this IServiceCollection services, string loginPath = "/Auth/Login", bool slidingExpiration = false, string accessDeniedPath = null, ITicketStore sessionStore = null, SameSiteMode cookieSameSite = SameSiteMode.Lax, string cookieDomain = null, CookieSecurePolicy cookieSecurePolicy = CookieSecurePolicy.SameAsRequest)
{
services.AddAuthentication(Saml2Constants.AuthenticationScheme)
.AddCookie(Saml2Constants.AuthenticationScheme, o =>
{
o.LoginPath = new PathString(loginPath);
o.SlidingExpiration = slidingExpiration;
if(!string.IsNullOrEmpty(accessDeniedPath))
{
o.AccessDeniedPath = new PathString(accessDeniedPath);
}
if (sessionStore != null)
{
o.SessionStore = sessionStore;
}
o.Cookie.SameSite = cookieSameSite;
o.Cookie.SecurePolicy = cookieSecurePolicy;
if (!string.IsNullOrEmpty(cookieDomain))
{
o.Cookie.Domain = cookieDomain;
}
});
return services;
}
So, to add SAML to your pipeline, you can remove the services.AddSaml2(), add the AddCookie section and, inside your policy, you can add the verification of any cookie with name saml2 to forward to your SAML schema.
services
.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = "custom-schema";
sharedOptions.DefaultChallengeScheme = "custom-schema";
})
.AddPolicyScheme("custom-schema", null, options =>
{
options.ForwardDefaultSelector = context =>
{
if (context.Request.Headers["Authorization"].Any(x => x.StartsWith("Bearer ")))
return JwtBearerDefaults.AuthenticationScheme;
else if (context.Request.Headers["Cookie"].Any(x => x.Contains(".saml2=")))
return Saml2Constants.AuthenticationScheme;
return "Identity.Application";
};
})
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, null, options =>
{
//...
})
.AddCookie(Saml2Constants.AuthenticationScheme, o =>
{
o.LoginPath = new PathString("/Auth/Login");
o.SlidingExpiration = false;
o.Cookie.SameSite = SameSiteMode.Lax;
o.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
});

Azure Mobile Services backend serviceUser does not return Facebook identities as expected

I'm struggling with a Xamarin Forms (iOS)/Azure Mobile Services/Facebook issue that I don't know how to resolve. What I'm trying to do is login to Facebook using AMS and then save that user's details to a service side database via a custom controller. I am running Azure Mobile Services on the backend.
What I have in place is the code below that successfully logs in a Facebook user.
var fbUser = await DependencyService.Get<IMobileClient>().LoginAsync(MobileServiceAuthenticationProvider.Facebook);
I then want to save fbUser to the database where I'm using ASP.NET Identity tables all configured. I want to use this user to gain access to the facebook user's profile information. I therefore have a backend service custom controller action that looks like this:
[Route("logintofacebook")]
[AuthorizeLevel(AuthorizationLevel.Anonymous)]
public async Task<IHttpActionResult> LoginToFacebook(MobileServiceUser msUser)
{
try
{
if (msUser != null)
{
var serviceUser = User as ServiceUser;
var identities = await serviceUser.GetIdentitiesAsync();
var result = new JObject();
var fb = identities.OfType<FacebookCredentials>().FirstOrDefault();
if (fb == null)
{
return NotFound();
}
var googleCredentials = identities.OfType<GoogleCredentials>().FirstOrDefault();
var azure = identities.OfType<MicrosoftAccountCredentials>().FirstOrDefault();
var accessToken = fb.AccessToken;
result.Add("facebook",
await GetProviderInfo("https://graph.facebook.com/me?access_token=" + accessToken));
var email = GetUserInfo(result);
var userTypeId = UTypes.Facebook;
When debugging on the backend side, the MobileServiceUser is a valid object and I can check the token and Facebook userid which are the same as were created on the client. However, the highlighted line returns zero identities. This means that fb variable end up being null.
The question is, why are no identities being returned from the serviceUser variable above?
Here's what the debugged AMS token looks like when debugged with jwt.io
{
"iss": "urn:microsoft:windows-azure:zumo",
"aud": "urn:microsoft:windows-azure:zumo",
"nbf": 1446872000,
"exp": 1449464000,
"urn:microsoft:credentials": "{\"accessToken\":\"CAAL2gwRM4RYBAKV9Wp0Evjp2aATnm5OIHHc15ujJfeevqCW6DoI36HOQCOYq96xUjZA6VXwovnkBOlY0SkC9nrdwr8jdbF3qJdtK4GAHVk9SGxKVYUZBJ4UwPqQmb5yka93GzL0Fl86m93LnqTffIPJ6vkMfpP0ZAroKzmcJxM1pJ7BAAAA\"}",
"uid": "Facebook:0000000000000000",
"ver": "2"
}
(I've replaced out the facebook section with zeros)
thanks
O

How to get Facebook Friend List in ASP.NET?

I'm building an App with ASP.NET MVC 5 and Identity.
So far the login is working correctly.
Here the auth:
var fb = new FacebookAuthenticationOptions();
fb.Scope.Add("email");
fb.Scope.Add("friends_about_me");
fb.Scope.Add("friends_photos");
fb.AppId = "";
fb.AppSecret = "";
fb.Provider = new FacebookAuthenticationProvider() {
OnAuthenticated = async FbContext => {
FbContext.Identity.AddClaim(
new System.Security.Claims.Claim("FacebookAccessToken", FbContext.AccessToken));
}
};
fb.SignInAsAuthenticationType = DefaultAuthenticationTypes.ExternalCookie;
app.UseFacebookAuthentication(fb);
I'm trying to get the friends list. I've been looking for a few examples but none is working with this version of MVC 5.
My question is. How can I fetch all the friends with this version?
I don't want to use Javascript API, I want all the code in c# and then send to the view.
I think I just need to rewrite the login and store the access token in the session, and then simply call var client = new FacebookClient(TOKEN);
So how can I rewrite the login?
You've already got everything you need. The OnAuthenticated callback you've set adds a claim containing the access token for Facebook. You just need to pull the claim for the user:
var identity = (ClaimsIdentity)User.Identity;
var facebookClaim = identity.Claims.FirstOrDefault(c => c.Type == "FacebookAccessToken");
if (facebookClaim != null)
{
// access facebook API with `facebookClaim.Value`
}
And if it exists, then you can use the Facebook API to pull in their friends by making standard HTTP calls via something like HttpClient.

Pass a ADFS token to a custom STS service

I am testing a product that authenticates uses using a custom STS service. The way it used to work is, when a user hits the website using the browser, we issue a redirect to hit the STS service. the STS service authenticates the user by hitting AD and then issues a SAML token with some custom claims for the user. The website then hits the STS once again to get a ActAs token so we can communicate with the data service.
And I had a automation that would mimic this behavior and its working fine in production.
We are not modifying the STS to use ADFS to authenticate instead of hitting the AD directly. So now when I hit the website, the request gets redirected to a ADFS endpoint which authenticates the user and issues a token. Then we hit the custom STS service that would use the token to authenticate the user (instead of hitting AD), add custom claims and issue a SAML token for the user. We then generate a ActAs token using this to finally hit the data service.
I am trying to update my automation for this changed behavior. So what I am doing now is hit the ADFS service, obtain a token and pass the token to the STS service so it can issue me a SAML token.
I am quite an amateur when it comes to windows identity service so i am having hard time trying to get this work. I have successfully obtained the token (Bearer Token) from the ADFS but i cant figureout how to pass this token to my custom STS so it can issue me a SAML token.
Any help would be highly appreciated. Thanks!
here is the code i am using
public static SecurityToken GetSecurityToken()
{
var endPoint = new EndpointAddress(new Uri(#"ADFS endpoint"));
var msgBinding = new WS2007HttpBinding(SecurityMode.TransportWithMessageCredential, false);
msgBinding.Security.Message.EstablishSecurityContext = false;
msgBinding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;
var factory = new WSTrustChannelFactory(msgBinding, endPoint);
factory.TrustVersion = TrustVersion.WSTrust13;
factory.Credentials.SupportInteractive = true;
factory.Credentials.UserName.UserName = "user";
factory.Credentials.UserName.Password = "pwd";
var rst = new RequestSecurityToken
{
RequestType = RequestTypes.Issue,
KeyType = KeyTypes.Bearer,
AppliesTo = new EndpointReference(#"custom STS endpoint")
};
return factory.CreateChannel().Issue(rst);
}
public static void GetUserClaimsFromSecurityTokenService(SecurityToken secToken)
{
var securityTokenManager = new SecurityTokenHandlerCollectionManager(string.Empty);
securityTokenManager[string.Empty] = SecurityTokenHandlerCollection.CreateDefaultSecurityTokenHandlerCollection();
var trustChannelFactory = new WSTrustChannelFactory(Binding, new EndpointAddress("custom STS endpoint"))
{
TrustVersion = TrustVersion.WSTrust13,
SecurityTokenHandlerCollectionManager = securityTokenManager,
};
var rst = new RequestSecurityToken(RequestTypes.Issue)
{
AppliesTo = new EndpointReference("website url"),
TokenType = SamlSecurityTokenHandler.Assertion
};
var channel = (WSTrustChannel)trustChannelFactory.CreateChannel();
channel.Open(TimeSpan.FromMinutes(15));
try
{
RequestSecurityTokenResponse rstr;
SecurityToken token = channel.Issue(rst, out rstr);
var genericToken = (GenericXmlSecurityToken)token;
var req = new SamlSecurityTokenRequirement();
var handler = new SamlSecurityTokenHandler(req)
{
Configuration = new SecurityTokenHandlerConfiguration()
};
var newToken = handler.ReadToken(new XmlNodeReader(genericToken.TokenXml));
}
finally
{
channel.Close();
}
}