Can't send logout request to Identity Server from MVC application - c#

in my application I accept owin authentication towards Identity Server. Everything works well, except the logout part. In practice when I want to logout I expect to be logged out also from my Identity server but this doesn't happen and I logout only from my application. The code is the following:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Login.aspx")
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
var AuthorityUrl = ConfigurationManager.AppSettings["AuthorityUrl"];
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = AuthorityUrl,
RedirectUri = $"{ConfigurationManager.AppSettings["PortalWebUrl"]}/signin-oidc",
PostLogoutRedirectUri = $"{ConfigurationManager.AppSettings["PortalWebUrl"]}/signout-callback-oidc",
RequireHttpsMetadata = false,
ClientId = "portal-local",
AuthenticationType = "oidc",
SignInAsAuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
ResponseType = "id_token token",
Scope = "openid profile email",
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = n =>
{
var claimsToExclude = new[]
{
"aud", "iss", "nbf", "exp", "nonce", "iat", "at_hash"
};
var claimsToKeep = n.AuthenticationTicket.Identity.Claims.Where(x => !claimsToExclude.Contains(x.Type)).ToList();
claimsToKeep.Add(new Claim("id_token", n.ProtocolMessage.IdToken));
var ci = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType, "name", "role");
ci.AddClaims(claimsToKeep);
n.AuthenticationTicket = new Microsoft.Owin.Security.AuthenticationTicket(ci, n.AuthenticationTicket.Properties);
return Task.CompletedTask;
},
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
{
n.ProtocolMessage.IdTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token")?.Value;
}
return Task.CompletedTask;
}
}
});
the "warning" coming back from Identity Server is the following:
Can you help me figuring out what am I doing wrong?

Related

Infinite redirects between Identity Server and Website

I've got the following client set up in IdentityServer:
internal class Clients
{
public static IEnumerable<Client> Get()
{
return new List<Client>
{
new Client
{
ClientId = "Client",
ClientName = "Client",
ClientSecrets = new List<Secret> {new Secret("blablabla".Sha256())},
AllowedGrantTypes = GrantTypes.Code,
RedirectUris = { Startup.Configuration.GetValue<string>("Clients", "https://localhost:5002") + "/signin-oidc" },
PostLogoutRedirectUris = { Startup.Configuration.GetValue<string>("Clients", "https://localhost:5002") + "/signout-oidc" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"roles",
"Business-to-Business-users",
"reporting-urls"
},
RequirePkce = true,
AllowPlainTextPkce = false,
AllowAccessTokensViaBrowser = true
}
};
}
}
I'm trying to modify the mvc client to login to the above identity server. In startup.cs I modified cllientID, ClientSecret and Authority to match the client settings. This is my ConfigureServices():
services.AddAuthentication(options =>
{
options.DefaultScheme = "cookie";
options.DefaultChallengeScheme = "oidc";
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie("cookie")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = Configuration.GetValue<string>("IdentityHost", "https://localhost:5000");
options.ClientId = "Client";
options.ClientSecret = "blablabla";
options.RequireHttpsMetadata = false;
options.GetClaimsFromUserInfoEndpoint = true;
options.ResponseType = "code";
options.UsePkce = true;
options.ResponseMode = "query";
options.Scope.Add("roles");
options.Scope.Add("Business-to-Business-users");
options.Scope.Add("reporting-urls");
options.RequireHttpsMetadata = false;
options.ClaimActions.MapUniqueJsonKey("role", "role");
options.ClaimActions.MapUniqueJsonKey("Business-to-Business-user", "Business-to-Business-user");
options.ClaimActions.MapUniqueJsonKey("reporting-url", "reporting-url");
options.TokenValidationParameters = new TokenValidationParameters
{
RoleClaimType = "role",
};
});
first I got a bug that he needs https but instead uses http. So I added the following code to my Configure:
app.Use((context, next) => {
context.Request.Scheme = "https";
return next();
});
Now that this error was solved, I ran into a different one.
As soon as I'm trying to login to my client, I got the following error in my browser (Opera): "ERR_TOO_MANY_REDIRECTS".
Does anyone have an idea how I can proceed? Both Identity and the client are seperate docker containers.

Identityserver4 and API in single project

I have an IdentityServer4 asp.net-core host setup for Resource Owner Password Grant using JWT Bearer tokens and an API in a separate asp.net-core host which has my API and two Angular clients.
The Authentication and Authorization is working from my two Angular clients to my API.
Now I need to expose an API in the IdentityServer4 host so I can create users from one of the Angular clients.
I have copied my Authentication and Authorization setup from my API over to my IdentityServer4 host, however, I cannot get it to Authenticate.
In the below code, within the API, I can set a breakpoint on the jwt.Authority... line and the first call will trigger this breakpoint in my API but not in the IdentityServer4 host.
Authentication
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddJwtBearer(jwt =>
{
jwt.Authority = config.Authentication.Authority; //Breakpoint here
jwt.RequireHttpsMetadata = config.Authentication.RequireHttpsMetadata;
jwt.Audience = Common.Authorization.Settings.ServerApiName;
});
Authorization
I'm not sure if it's relevant, but I'm using role based authorization, the following is the setup for this.
var authPolicyBuilder = new AuthorizationPolicyBuilder()
.RequireRole(Common.Authorization.Settings.ServerApiRoleBasePolicyName)
.Build();
services.AddMvc(options =>
{
options.Filters.Add(new AuthorizeFilter(authPolicyBuilder));
...
services.AddAuthorization(options =>
{
options.AddPolicy(Common.Authorization.Settings.ServerApiSetupClientAdminRolePolicyName, policy =>
{
policy.RequireClaim("role", Common.Authorization.Settings.ServerApiSetupClientAdminRolePolicyName);
I've extracted the following from my logging:
What I see is that in the non-working case, I never get to the point of invoking the JWT validation (#3 in the working logs).
This is just a tiny extract of my logs, I can share them in entirety if needs be.
Working
1 Request starting HTTP/1.1 GET http://localhost:5100/packages/
(SourceContext:Microsoft.AspNetCore.Hosting.Internal.WebHost)
2 Connection id "0HLC8PLQH2NRU" started.
(SourceContext:Microsoft.AspNetCore.Server.Kestrel)
3 Request starting HTTP/1.1 GET http://localhost:5000/.well-known/openid-configuration
(SourceContext:Microsoft.AspNetCore.Hosting.Internal.WebHost)
--Truncated--
Not Working
1 Request starting HTTP/1.1 GET http://localhost:5000/users
(SourceContext:Microsoft.AspNetCore.Hosting.Internal.WebHost)
--Truncated--
Clients
new Client
{
ClientId = "setup_app",
ClientName = "Setup App",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
AccessTokenType = AccessTokenType.Jwt,
AccessTokenLifetime = 3600,
IdentityTokenLifetime = 3600,
UpdateAccessTokenClaimsOnRefresh = true,
SlidingRefreshTokenLifetime = 3600,
AllowOfflineAccess = false,
RefreshTokenExpiration = TokenExpiration.Absolute,
RefreshTokenUsage = TokenUsage.OneTimeOnly,
AlwaysSendClientClaims = true,
Enabled = true,
RequireConsent = false,
AlwaysIncludeUserClaimsInIdToken = true,
AllowedCorsOrigins = { config.CorsOriginSetupClient },
ClientSecrets =
{
new Secret(Common.Authorization.Settings.ServerApiSetupClientSecret.Sha256())
},
AllowedScopes =
{
Common.Authorization.Settings.ServerApiName,
}
},
new Client
{
ClientId = "client_app",
ClientName = "Client App",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
AccessTokenType = AccessTokenType.Jwt,
AccessTokenLifetime = 3600,
IdentityTokenLifetime = 3600,
UpdateAccessTokenClaimsOnRefresh = true,
SlidingRefreshTokenLifetime = 3600,
AllowOfflineAccess = false,
RefreshTokenExpiration = TokenExpiration.Absolute,
RefreshTokenUsage = TokenUsage.OneTimeOnly,
AlwaysSendClientClaims = true,
Enabled = true,
RequireConsent = false,
AlwaysIncludeUserClaimsInIdToken = true,
AllowedCorsOrigins = { config.CorsOriginSetupClient },
ClientSecrets =
{
new Secret(Common.Authorization.Settings.ServerApiAppClientSecret.Sha256())
},
AllowedScopes =
{
Common.Authorization.Settings.ServerApiName,
}
}
IdentityResources
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResource(Common.Authorization.Settings.ServerApiScopeName, new []{
"role",
Common.Authorization.Settings.ServerApiSetupClientAdminRolePolicyName,
Common.Authorization.Settings.ServerApiAppClientAdminRolePolicyName,
Common.Authorization.Settings.ServerApiAppClientUserRolePolicyName,
}),
};
User
var adminUser = new ApplicationUser
{
UserName = "admin",
Email = "admin#noreply",
};
adminUser.Claims = new List<IdentityUserClaim>
{
new IdentityUserClaim(new Claim(JwtClaimTypes.PreferredUserName, adminUser.UserName)),
new IdentityUserClaim(new Claim(JwtClaimTypes.Email, adminUser.Email)),
new IdentityUserClaim(new Claim("role", Common.Authorization.Settings.ServerApiSetupClientAdminRolePolicyName)),
new IdentityUserClaim(new Claim("role", Common.Authorization.Settings.ServerApiRoleBasePolicyName)),
new IdentityUserClaim(new Claim("profileImage", $"https://robohash.org/{Convert.ToBase64String(System.Security.Cryptography.MD5.Create().ComputeHash(System.Text.Encoding.UTF8.GetBytes(adminUser.UserName)))}?set=set2"))
};
adminUser.AddRole(Common.Authorization.Settings.ServerApiSetupClientAdminRolePolicyName);
API
new ApiResource(Common.Authorization.Settings.ServerApiName, "Server API"){
ApiSecrets =
{
new Secret(Common.Authorization.Settings.ServerApiAppClientSecret.Sha256())
},
},
Look up here https://github.com/IdentityServer/IdentityServer4.Samples
Seems like it should be like:
Authentication:
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = config.Authentication.Authority;
options.RequireHttpsMetadata = false;
options.ApiName = ServerApiName;
options.ApiSecret = ServerApiAppClientSecret;
});
Or with JWT you can try like:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = config.Authentication.Authority;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = true,
ValidAudiences = new[]
{
$"{config.Authentication.Authority}/resources",
ServerApiName
},
};
});
Also, you will able to add authorization policy, like:
Authorization:
services.AddMvc(opt =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.RequireScope("api").Build();
opt.Filters.Add(new AuthorizeFilter(policy));
})
There is a MS out of the box service designed to add support for local APIs
First in IDS4 startup.cs, ConfigureServices add
services.AddLocalApiAuthentication();
Then in IDS4 config.cs in your client declaration
AllowedScopes = {
IdentityServerConstants.LocalApi.ScopeName, <<<< ---- This is for IDS4 Api access
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email
...
Still in config add the new scope to ApiScopes
new ApiScope(IdentityServerConstants.LocalApi.ScopeName)
Note
IdentityServerConstants.LocalApi.ScopeName resolves to 'IdentityServerApi'
Now in your shiny new Api located in the IDS4 project add the authorize tag to your api endpoint
[Authorize(IdentityServerConstants.LocalApi.PolicyName)]
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
Finally you need to request this new scope in your Client
Scope = "openid sig1 api1 profile email offline_access company IdentityServerApi",
Thats it
I think this could help, it's included in the community samples.

Identity Server 4 and external provider returning plain HTML if unauthenticated

We have a machine running IdentityServer4 which itself is used as a federated gateway to Office 365.
I'm trying to figure out how to circumvent this scenario: we create an MVC-app that has the following owin-startup class:
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationType = "Cookies",
ExpireTimeSpan = TimeSpan.FromMinutes(1),
SlidingExpiration = false
});
JwtSecurityTokenHandler.InboundClaimTypeMap.Clear();
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "oidc",
SignInAsAuthenticationType = "Cookies",
Authority = "https://localhost:5000/",
ClientId = "dev",
RedirectUri = "http://localhost:54509/",
ResponseType = "id_token token",
Scope = "openid profile account",
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = async n =>
{
var claims_to_exclude = new[]
{
"aud", "iss", "nbf", "exp", "nonce", "iat", "at_hash"
};
var claims_to_keep =
n.AuthenticationTicket.Identity.Claims
.Where(x => false == claims_to_exclude.Contains(x.Type)).ToList();
claims_to_keep.Add(new Claim("id_token", n.ProtocolMessage.IdToken));
var ci = new ClaimsIdentity(
n.AuthenticationTicket.Identity.AuthenticationType,
"name", "role");
ci.AddClaims(claims_to_keep);
n.AuthenticationTicket = new AuthenticationTicket(
ci, n.AuthenticationTicket.Properties
);
},
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
{
var id_token = n.OwinContext.Authentication.User.FindFirst("id_token")?.Value;
n.ProtocolMessage.IdTokenHint = id_token;
}
return Task.FromResult(0);
}
}
});
app.UseStageMarker(PipelineStage.Authenticate);
So far so good: upon first visiting the user is redirected to the Identity Server, signing in and getting back but when they lose their token or become unauthenticated and try to access a controller decorated with [Authorize] the plain HTML of the sign-on site is returned to that controller.
Are we using the wrong flow for this back channel, server-to-server communication? Or is there a way to intercept this so that they can be redirected immediately before the controller freaks out over getting HTML instead of JSON.

Map IdentityServer 4 claims to .NET MVC 4 client

during this week i'm trying to get my client connected to my IdentityServer 4.
On the IdentityServer i've implemented the IProfileService to set some static claims on a user.
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var sub = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(sub);
var principal = await _userClaimsPrincipalFactory.CreateAsync(user);
var claims = principal.Claims.Where(claim => context.RequestedClaimTypes.Contains(claim.Type)).ToList();
context.IssuedClaims = claims;
var claims2 = new List<Claim>
{
new Claim("role", "JustSpend.Employee"),
new Claim("role", "JustSpend.Administrator"),
new Claim("fullName", string.Format("{0} {1}", user.FirstName, user.LastName)),
new Claim("canAccess", "JustSpend"),
new Claim("canAccess", "APRA"),
new Claim("canAccess", "SFVO")
};
context.IssuedClaims.AddRange(claims2);
}
At the MVC 4 client side I have the following configuration:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "NIC Identity",
Authority = "http://localhost:5000", //ID Server
ClientId = "JSpend",
ResponseType = "id_token code",
SignInAsAuthenticationType = "Cookies",
RedirectUri = "http://localhost:57895/signin-oidc", //URL of website
PostLogoutRedirectUri = "http://localhost:57895/",
Scope = "openid profile offline_access",
TokenValidationParameters = new TokenValidationParameters() { NameClaimType = "name", RoleClaimType = "role" },
ClientSecret = "secret",
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
var discoveryClient = new DiscoveryClient("http://localhost:5000");
var doc = await discoveryClient.GetAsync();
// use the code to get the access and refresh token
var tokenClient = new TokenClient(
doc.TokenEndpoint,
"JSpend",
"secret");
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(
n.Code, n.RedirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
// use the access token to retrieve claims from userinfo
var userInfoClient = new UserInfoClient(doc.UserInfoEndpoint);
var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);
// create new identity
var id = n.AuthenticationTicket.Identity;
//id.AddClaims(userInfoResponse.Claims);
id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToLocalTime().ToString()));
id.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken));
id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
id.AddClaim(new Claim("sid", n.AuthenticationTicket.Identity.FindFirst("sid").Value));
n.AuthenticationTicket = new AuthenticationTicket(
new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType, "name", "role"),
n.AuthenticationTicket.Properties);
},
RedirectToIdentityProvider = n =>
{
// if signing out, add the id_token_hint
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
}
return Task.FromResult(0);
}
}
});
The claims were retreived all fine in the userInfoResponse. After the user have signed in, he is redirected to the AccountController (ExternalLoginCallback). But the claims are not mapped on the User in the controller at all.
ExternalLoginCallback method:
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
{
return RedirectToAction("Login");
}
// Sign in the user with this external login provider if the user already has a login
var result = await _signInManager.ExternalSignInAsync(loginInfo, isPersistent: false);
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = false });
case SignInStatus.Failure:
default:
// If the user does not have an account, then prompt the user to create an account
ViewBag.ReturnUrl = returnUrl;
ViewBag.LoginProvider = loginInfo.Login.LoginProvider;
return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = loginInfo.Email });
}
}
Who can help me out? I'm looking for a solution the whole week, and if I Google, all the searchresults are turned purple (as visited)...

Redirect to login with attribute Authorize using cookies authentication in ASP.NET 5

I am testing the [Authorize] attribute, but I can't make a redirect to login page if the user has not logged yet (the Chrome inspector returns a 401).
This is my code to make the login in my Controller (very simple).
if (model.UserName == "admin" && model.Password == "test")
{
var claims = new[] { new Claim("name", model.UserName), new Claim(ClaimTypes.Role, "Admin") };
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.Authentication.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity));
return RedirectToAction("Index", "Home");
}
And this is my configuration in the Startup.cs for logins:
app.UseCookieAuthentication(options =>
{
options.AutomaticAuthenticate = true;
options.LoginPath = new PathString("/Account/Login");
});
Any ideas?
Thanks!!
Your Startup.cs should look like the following:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
LoginPath = "/account/login",
AuthenticationScheme = "Cookies",
AutomaticAuthenticate = true,
AutomaticChallenge = true
});
Setting the AutomaticChallenge is what is going to make the [Authorize] attribute work. Be sure to include the [Authorize] attribute on any of the controllers you want the redirect (302) to happen.
There is a very basic sample in this GitHub repo that might provide some guidance:
https://github.com/leastprivilege/AspNet5TemplateCookieAuthentication
Try this in the Startup.cs:
app.UseCookieAuthentication(options =>
{
options.AuthenticationType = CookieAuthenticationDefaults.AuthenticationScheme;
options.AutomaticAuthenticate = true;
options.AutomaticChallenge = true;
options.LoginPath = new PathString("/Account/Login");
});
And this in the Controller
IAuthenticationManager authManager = Request.GetOwinContext().Authentication;
authManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);

Categories

Resources