Both OIDC and ADFS in same application (OWIN) - asp.net-mvc-5

I am working on an ASP.NET MVC 5 website using EPiServer CMS.
The requirement is that we have admin (back-end users) log in using ADFS (working) and OIDC for front-end.
Both are set to passive mode and are called through action methods in a controller.
The issue I am facing is that front-end users OIDC is set to (Authentication type ="Federation") when it returns from external call instead of external cookie.
public void Configuration(IAppBuilder app)
{
//IdentityModelEventSource.ShowPII = true;
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11;
ConfigureMemberAuth(app);
ConfigureAdminAuth(app);
}
private void ConfigureMemberAuth(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/logmein/"),
LogoutPath = new PathString("/"),
AuthenticationMode = AuthenticationMode.Active
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
_ = app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = ApplicationSettings.OIDCClientId,
ClientSecret = ApplicationSettings.OIDCClientSecret,
Authority = ApplicationSettings.OIDCAuthority,
RedirectUri = $"{ApplicationSettings.Domain}{_oidcRedirectMethod}",
PostLogoutRedirectUri = ApplicationSettings.Domain,
ResponseType = OpenIdConnectResponseType.CodeIdToken,
Scope = "xxx",
SaveTokens = true,
AuthenticationMode = AuthenticationMode.Passive,
AuthenticationType = OpenIdConnectAuthenticationDefaults.AuthenticationType,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = notification =>
{
if (string.Equals(notification.ProtocolMessage.Error, "xxx", StringComparison.Ordinal))
{
notification.HandleResponse();
if (string.Equals(notification.ProtocolMessage.ErrorDescription, "xxx", StringComparison.Ordinal))
notification.Response.Redirect(ApplicationSettings.Domain);
else
{
var errorPage = UrlResolver.GetUrl(PageHelper.GetAdminPage().LoginBox.NemIDErrorPage.GetFriendlyUrl());
notification.Response.Redirect(errorPage);
}
}
return Task.CompletedTask;
},
// Retrieve an access token from the remote token endpoint
// using the authorization code received during the current request.
AuthorizationCodeReceived = async notification =>
{
using (var client = new HttpClient())
{
var configuration = await notification.Options.ConfigurationManager.GetConfigurationAsync(notification.Request.CallCancelled);
var tokenEndpointResult = await ExchangeCodeForTokens(notification, client, configuration);
// Add the identity token to the returned ClaimsIdentity to make it easier to retrieve.
notification.AuthenticationTicket.Identity.AddClaim(new Claim(
type: OpenIdConnectParameterNames.IdToken,
value: tokenEndpointResult.Value<string>(OpenIdConnectParameterNames.IdToken)));
// Retrieve the claims from UserInfo endpoint using the access token as bearer token.
var accesstoken = tokenEndpointResult.Value<string>(OpenIdConnectParameterNames.AccessToken);
var userInfoEndpointResult = await UserInfoEndpointClaims(notification, client, configuration, accesstoken);
//Security note: It is important to verify that the sub claim from ID token matches the sub claim in the UserInfo response
var userinfoSub = userInfoEndpointResult["xx"].Value<string>();
var idtokenSub = notification.AuthenticationTicket.Identity.FindFirst("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier").Value;
if (userinfoSub == idtokenSub)
{
//add claims from UserInfo endpoint to identity
foreach (var entry in userInfoEndpointResult)
{
if (!notification.AuthenticationTicket.Identity.HasClaim(c => c.Type == entry.Key))
{
notification.AuthenticationTicket.Identity.AddClaim(new Claim(
type: entry.Key,
value: entry.Value.ToString()));
}
}
// Add access token to claims.
notification.AuthenticationTicket.Identity.AddClaim(new Claim(
OpenIdConnectParameterNames.AccessToken,
accesstoken));
}
}
},
// Attach the id_token stored in the authentication cookie to the logout request.
RedirectToIdentityProvider = notification =>
{
if (notification.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
{
var token = notification.OwinContext.Authentication.User?.FindFirst(OpenIdConnectParameterNames.IdToken);
if (token != null)
{
notification.ProtocolMessage.IdTokenHint = token.Value;
}
notification.Response.Redirect(ApplicationSettings.Domain);
}
return Task.CompletedTask;
},
}
});
}
private void ConfigureAdminAuth(IAppBuilder app)
{
//Enable cookie authentication, used to store the claims between requests
app.SetDefaultSignInAsAuthenticationType(WsFederationAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = WsFederationAuthenticationDefaults.AuthenticationType,
CookieName = WsFederationAuthenticationDefaults.CookieName,
AuthenticationMode = AuthenticationMode.Active,
CookieHttpOnly = true,
});
//Enable federated authentication
_ = app.UseWsFederationAuthentication(new WsFederationAuthenticationOptions()
{
//Trusted URL to federation server meta data
MetadataAddress = ApplicationSettings.MetaDataAddress,
//Value of Wtreal must *exactly* match what is configured in the federation server
Wtrealm = ApplicationSettings.RelyPartyUri,
AuthenticationMode = AuthenticationMode.Passive,
Notifications = new WsFederationAuthenticationNotifications()
{
RedirectToIdentityProvider = (ctx) =>
{
// To avoid a redirect loop to the federation server send 403 when user is authenticated but does not have access
if (ctx.OwinContext.Response.StatusCode == 401 && (ctx.OwinContext.Authentication.User.Identity.AuthenticationType == WsFederationAuthenticationDefaults.AuthenticationType && ctx.OwinContext.Authentication.User.Identity.IsAuthenticated))
{
ctx.OwinContext.Response.StatusCode = 403;
ctx.HandleResponse();
}
//XHR requests cannot handle redirects to a login screen, return 401
if (ctx.OwinContext.Response.StatusCode == 401 && IsXhrRequest(ctx.OwinContext.Request))
{
ctx.HandleResponse();
}
return Task.FromResult(0);
},
SecurityTokenValidated = (ctx) =>
{
//Ignore scheme/host name in redirect Uri to make sure a redirect to HTTPS does not redirect back to HTTP
var redirectUri = new Uri(ctx.AuthenticationTicket.Properties.RedirectUri, UriKind.RelativeOrAbsolute);
if (redirectUri.IsAbsoluteUri)
{
ctx.AuthenticationTicket.Properties.RedirectUri = redirectUri.PathAndQuery;
}
//Sync user and the roles to EPiServer in the background
ServiceLocator.Current.GetInstance<ISynchronizingUserService>().SynchronizeAsync(ctx.AuthenticationTicket.Identity);
return Task.FromResult(0);
},
}
});
//Add stage marker to make sure WsFederation runs on Authenticate (before URL Authorization and virtual roles)
app.UseStageMarker(PipelineStage.Authenticate);
// Remap logout to a federated logout
app.Map(LogoutUrl, map =>
{
map.Run(ctx =>
{
ctx.Authentication.SignOut();
ctx.Response.Redirect(ApplicationSettings.Domain);
return Task.FromResult(0);
});
});
}
When I out comment this line: app.SetDefaultSignInAsAuthenticationType(WsFederationAuthenticationDefaults.AuthenticationType);
in admin auth then client-login works as intended and authentication type is set to "ExternalCookie" when call return but then admin login stops working ?
Any help would be appreciated

Related

Unable to redirect uri after cas authentication with WSO2 provider using asp.net core authorization

[enter image description here][1]I am trying to authenticate my .net core application using cas with Wso2 identity provider although the autentication is succesful but on redirect uri i am getting webpage not found error with cas ticket not able to figure out what is the issue any guidance would be helpful
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Login";
options.Events.OnSigningOut = context =>
{
var redirectContext = new RedirectContext<CookieAuthenticationOptions>(
context.HttpContext,
context.Scheme,
context.Options,
context.Properties,
"/"
);
if (builder.Configuration.GetValue("Authentication:CAS:SingleSignOut", false))
{
// Single Sign-Out
var casUrl = new Uri(builder.Configuration["Authentication:CAS:ServerUrlBase"]);
var links = context.HttpContext.RequestServices.GetRequiredService<LinkGenerator>();
var serviceUrl = context.Properties.RedirectUri ?? links.GetUriByPage(context.HttpContext, "/Index");
redirectContext.RedirectUri = UriHelper.BuildAbsolute(
casUrl.Scheme,
new HostString(casUrl.Host, casUrl.Port),
casUrl.LocalPath, "/logout",
QueryString.Create("service", serviceUrl!));
}
context.Options.Events.RedirectToLogout(redirectContext);
return Task.CompletedTask;
};
})
.AddCAS(o =>
{
o.CasServerUrlBase = builder.Configuration["Authentication:CAS:ServerUrlBase"]; // Set in `appsettings.json` file.
o.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
o.CasValidationUrl = "/";
});
Login controller
[HttpGet("login")]
public IActionResult Login(string returnUrl)
{
// var props = new AuthenticationProperties { RedirectUri = "/" };
return Challenge(new AuthenticationProperties { RedirectUri = returnUrl }, "CAS");
// return Redirect("http://localhost:5095/");
}
[Web Response](https://i.stack.imgur.com/1LFOP.png)
[1]: https://i.stack.imgur.com/v3fKm.png

Authorization Code Flow Error in ASP.Net Form using Owin Starup

We are implementing Azure SSO in Traditional ASP.Net Web Application and we want to implement Authorization Code Flow for generating Refresh, Access and Id Tokens.
We have implemented the below code in AuthorizationCodeReceived function of the owin's app.UseOpenIdConnectAuthentication class. From the below mentioned code we are able to successfully fetch the Refreshtoken, AccessToken and IdToken.
But notification.AuthenticationTicket is null and it throws null reference excpetion so we are not able to add the claims for id and access tokens.
Also in the aspx.cs file the HttpContext.Current.User.Identity.IsAuthenticated is returned as false even after generating all the 3 tokens.
Please suggest why notification.AuthenticationTicket is null inside AuthorizationCodeReceived event and what changes we have to do inside AuthorizationCodeReceived event to make HttpContext.Current.User.Identity.IsAuthenticated as "true".
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseKentorOwinCookieSaver();
app.UseCookieAuthentication(new CookieAuthenticationOptions());
Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectProtocolValidator dd = new Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectProtocolValidator();
dd.RequireNonce = false;
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
ClientSecret = clientSecret,
PostLogoutRedirectUri = redirectUri,
RedirectUri = redirectUri,
Scope = "openid profile email offline_access",
ResponseType = OpenIdConnectResponseType.Code,
SignInAsAuthenticationType = "Cookies",
Notifications = new OpenIdConnectAuthenticationNotifications()
{
AuthenticationFailed = (context) =>
{
return System.Threading.Tasks.Task.FromResult(0);
},
AuthorizationCodeReceived = async notification =>
{
using (var client = new HttpClient())
{
var configuration = await notification.Options.ConfigurationManager.GetConfigurationAsync(notification.Request.CallCancelled);
var request = new HttpRequestMessage(HttpMethod.Post, configuration.TokenEndpoint);
request.Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
[OpenIdConnectParameterNames.ClientId] = notification.Options.ClientId,
[OpenIdConnectParameterNames.ClientSecret] = notification.Options.ClientSecret,
[OpenIdConnectParameterNames.Code] = notification.ProtocolMessage.Code,
[OpenIdConnectParameterNames.GrantType] = "authorization_code",
[OpenIdConnectParameterNames.RedirectUri] = notification.Options.RedirectUri
});
var response = await client.SendAsync(request, notification.Request.CallCancelled);
response.EnsureSuccessStatusCode();
var payload = JObject.Parse(await response.Content.ReadAsStringAsync());
notification.AuthenticationTicket.Identity.AddClaim(new Claim(
type: OpenIdConnectParameterNames.AccessToken,
value: payload.Value<string>(OpenIdConnectParameterNames.AccessToken)));
notification.AuthenticationTicket.Identity.AddClaim(new Claim(
type: OpenIdConnectParameterNames.IdToken,
value: payload.Value<string>(OpenIdConnectParameterNames.IdToken)));
}
},
// Attach the id_token stored in the authentication cookie to the logout request.
RedirectToIdentityProvider = notification =>
{
if (notification.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
{
var token = notification.OwinContext.Authentication.User?.FindFirst(OpenIdConnectParameterNames.IdToken);
if (token != null)
{
notification.ProtocolMessage.IdTokenHint = token.Value;
}
}
return Task.CompletedTask;
},
SecurityTokenValidated = (context) =>
{ if (context != null)
{
if (context.ProtocolMessage != null && !string.IsNullOrEmpty(context.ProtocolMessage.IdToken))
{
context.AuthenticationTicket.Identity.AddClaim(new Claim("IdToken", context.ProtocolMessage.IdToken));
}
}
return Task.FromResult(0);
}
}
}
);

HttpContext.Request.IsAuthenticated is false in Ajax request + OpenID Connect Cookie based validation

We are using OpenIdConnect based authentication in the asp.net mvc application. Initial login is working fine. But when we use the Ajax call to invoke the action method, User is coming as not authenticated. I checked in Custom Authorization - HttpContext.Request.IsAuthenticated is coming as false.
I checked the cookie ".AspNet.Cookies" and it has the value. Why is open ID not authenticating the user.
Below is my authentication code
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = AuthenticationConfig.ClientId,
Authority = AuthenticationConfig.AADInstance + AuthenticationConfig.TenantId,
PostLogoutRedirectUri = AuthenticationConfig.PostLogoutRedirectURI,
RedirectUri = AuthenticationConfig.RedirectUri,
Scope = OpenIdConnectScope.OpenIdProfile,
ResponseType = OpenIdConnectResponseType.Code,
SaveTokens = true,
TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(AuthenticationConfig.ClientSecret)),
ValidateIssuer = true,
ValidIssuer = AuthenticationConfig.AADInstance + AuthenticationConfig.TenantId + "/v2.0",
},
SignInAsAuthenticationType = "Cookies",
Notifications = new OpenIdConnectAuthenticationNotifications()
{
// when an auth code is received...
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = OnAuthenticationFailed,
SecurityTokenValidated = async n =>
{
var nid = new ClaimsIdentity(n.AuthenticationTicket.Identity);
//var claimsIdentity = filterContext.HttpContext.User.Identity as ClaimsIdentity;
var user = nid.Claims.Where(r => r.Type == PreferedUserNameClaimType).Select(v => v.Value).FirstOrDefault();
var userRolesroles = GetRolesForUser(user);
//nid.AddClaim(new Claim("expires_at", DateTimeOffset.Now.AddSeconds(int.Parse(n.ProtocolMessage.ExpiresIn)).ToString()));
userRolesroles.ToList().ForEach(ui => nid.AddClaim(new Claim(ClaimTypes.Role, ui)));
// keep the id_token for logout
nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
// add access token for sample API
nid.AddClaim(new Claim("access_token", n.ProtocolMessage.AccessToken));
n.AuthenticationTicket = new AuthenticationTicket(
nid,
n.AuthenticationTicket.Properties);
UserService.SetUserInformation(user);
},
RedirectToIdentityProvider = ctx =>
{
bool isAjaxRequest = (ctx.Request.Headers != null && ctx.Request.Headers["X-Requested-With"] == "XMLHttpRequest");
if (ctx.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication)
{
if (isAjaxRequest && ctx.Response.StatusCode == (int)HttpStatusCode.Unauthorized)
{
ctx.Response.Headers.Remove("Set-Cookie");
ctx.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
ctx.HandleResponse();
return Task.FromResult(0);
}
}
return Task.FromResult(0);
}
}
});
}
Usually in asp.net, the ApiControllers has no concept of your Controller's authentication. Depending on the way things are builr, you need to add an Authorization header with a bearer access token to let the API know about the authenticated user.

Pass user claims to downstream (secondary) APIs?

I have tried searching, but surprisingly cannot find an answer to my question.
I am designing a web app, that will have a front end interface via Angular, with multiple down stream APIs. Like below:
[API - A Client] -> [API - A] -> [API - B]
I am using IdentityServer4 for authentication / authorization. Some users will have a particular claim, lets call it "Foo," and that claim gets correctly passed from the auth server to API A (using Implicit flow) when interacting with API A via the SPA client.
However, I can't get that claim to be passed along from API A to API B, which is using Client Credentials. From what I have read / research, this seems to be correct behavior, since its Client Credential flow.
So my question is, how can I pass a User claim ("Foo"), downstream to a second layer API (API-B)? do i need to use a different flow? Should API-A manually pass it along the request to API-B?
This is my first time using IdentityServer / OpenID connect / OAuth, I am open to changes.
IdentityServer4 Config
public class Config
{
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("API-B", "API B")
{
UserClaims = { "Foo" }
},
new ApiResource("API-A", "API A")
{
ApiSecrets = {new Secret("Secret") },
UserClaims = { "Foo", },
}
};
}
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientName = "API-A Client",
ClientId = "API-A_client",
AllowedGrantTypes = GrantTypes.Implicit,
RedirectUris = { "http://localhost:7900/swagger/oauth2-redirect.html" },
PostLogoutRedirectUris = { "http://localhost:7900/" },
RequireConsent = false,
AllowAccessTokensViaBrowser = true,
AllowedScopes = new List<string>(){
"API-A",
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
}
},
new Client
{
ClientName = "API-A Backend",
ClientId = "API-A_backend",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets = {new Secret("Secret".Sha256()) },
AllowedScopes = new List<string>()
{
"API-B",
"custom_resource",
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
},
AlwaysIncludeUserClaimsInIdToken = true,
AlwaysSendClientClaims = true,
}
};
}
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResource("custom_resource", new [] { "Foo" }),
};
}
}
API A Auth Config
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:6900";
options.ApiName = "API-A";
options.RequireHttpsMetadata = false; // dev only!
});
services.AddTransient<AccessTokenDelegatingHandler>((service) => new AccessTokenDelegatingHandler(tokenEndpoint: $"http://localhost:6900/connect/token", clientId: "API-A", clientSecret: "Secret", scope: "API-B"));
services.AddHttpClient<ApiBHttpClient>(client =>
{
client.BaseAddress = new Uri(Configuration["ApiBUri"]);
client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddHttpMessageHandler<AccessTokenDelegatingHandler>();
API B Auth Config
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:6900";
options.ApiName = "API-B"; // required audience of access tokens
options.RequireHttpsMetadata = false; // dev only!
options.ApiSecret = "Secret";
});
The result above is API-A correctly gets access to "Foo" via IdentityClaims, however API-B does not (although the call is successful).
Any help is appreciated!
Finally found this GitHub page, asking the same question: https://github.com/IdentityServer/IdentityServer4/issues/1679
Which leads here, about extension grants, http://docs.identityserver.io/en/release/topics/extension_grants.html which is my exact scenario.

How do you get md:NameIDFormat to appear in the SAML2 Metadata output?

I have my SAML2 "working" (authentication: success) but shibboleth isn't sending me any claim data, I need just the users email :)
The shibboleth people are telling me to add this to my SAML2 metadata... it's very clearly not there.
<md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-
format:emailAddress</md:NameIDFormat>
We're using the OWIN middleware from https://github.com/Sustainsys/Saml2/ to get this all to work, but it's pretty stock config?
additionalProviders["saml2p"] =
(IAppBuilder app, string signInAsType, AuthenticationProviderElement config) =>
{
var opt = new Saml2AuthenticationOptions(false)
{
SPOptions = new SPOptions
{
EntityId = new EntityId("https://my.site.ca")
},
SignInAsAuthenticationType = signInAsType,
AuthenticationType = "saml2p",
Caption = "MySite",
Notifications = new Saml2Notifications()
{
AcsCommandResultCreated = (result, response) =>
{
var claimsIdentity = result.Principal.Identity as ClaimsIdentity;
//None of this exists in the result
var userEmail = claimsIdentity.Claims.FirstOrDefault(x => x.Type == "User.email");
var userFirstName = claimsIdentity.Claims.FirstOrDefault(x => x.Type == "User.FirstName");
var userLastName = claimsIdentity.Claims.FirstOrDefault(x => x.Type == "User.LastName");
},
LogoutCommandResultCreated = commandResult =>
{
// Post logout URL
commandResult.Location = new Uri("/login", UriKind.Relative);
}
},
};
Sustainsys.Saml2.Configuration.Options.GlobalEnableSha256XmlSignatures();
opt.IdentityProviders.Add(new IdentityProvider(
new EntityId("https://their.site.ca/shibboleth-idp/shibboleth"),
opt.SPOptions)
{
LoadMetadata = true
});
app.UseSaml2Authentication(opt);
};
return additionalProviders;
TL;DR; md:NameIDFormat not in SustainSys SAML2 metadata output
The config on my end was correct, the problem was the config in shibboleth not sending the http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier claim back.

Resources