Sign Out of a Blazor App using the OIDC Authentication Scheme - c#

We have a .NET Core 6 Blazor Server App. We login with our own Identity Provider using OIDC. We are having an issue signing out.
We have set up our authentication using the following code block.
builder.Services
.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddCookie()
.AddOpenIdConnect(opts => {
opts.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
opts.RequireHttpsMetadata = !isDebug;
opts.ClientId = "user-accounts-app";
opts.CallbackPath = "/signin-oidc";
opts.ResponseType = OpenIdConnectResponseType.Code;
opts.Authority = authority;
opts.ClientSecret = builder.Configuration["CLIENT_SECRET"];
var scopes = new List<string>() {
"openid", "profile", "email", "phone", "offline_access"
};
foreach(var s in scopes)
{
opts.Scope.Add(s);
}
});
The discovery document does include an end_session_endpoint; however, the endpoint is never hit. We attempt to signout from a razor page with
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
// This line does not work
await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties
{
RedirectUri = "http://mydomainhere.com/our/path/here",
});
Running that 2nd SignOutAsync seems to do nothing. The Identity Provider is not hit at the end session endpoint and nothing happens on our logout page. Our session is not cleared from the IDP.
Additionally, our cookies for the blazor app are not entirely cleared. We have a ton of lingering .AspNetCorrelation.hash<hash-here> with path /signin-oidc (tried to get a screenshot but SO is having server errors with those right now). But the .AspNetCore cookie is cleared successfully by the first SignOutAsync call.
I'm not sure what the behavior of the second SignOutAsync is supposed to be. Would it redirect the user to the logout url of the IDP? Or does it do that in the background? Are we missing some configuration in our call to AddOpenIdConnect() to handle sign out?

Looks like we were just missing an OIDC sign out scheme.
opts.SignOutScheme = OpenIdConnectDefaults.AuthenticationScheme;
This is all we needed to get it working.
ASP.net will use the sign in scheme if no sign out scheme is specified. The sign in scheme is cookie which is a bit misleading because the OpenID authority is actually the one you're signed into. Signing in leads to the cookie being created to store the auth token provided by that authority (so you are effectively signed into the client app). If you sign out with a cookie scheme then only the cookie is destroyed -- you are signed out of the client, but not the authority. The next time you come to a page, you just get a new cookie because you're already signed into the authority.
The sign out scheme above therefore signs out of the authority, not just the client. I'm not sure if my colleague who figured it out also added a step of removing the cookie. I will edit this with details if I find out they did or not. It may somehow be magically handled by the asp framework.

Related

How to Authenticate two subdomain by one login in IdentityServer?

I have an IDP server implemented by Duende IdentityServer assume which is hosted on idp.com and there are two separate ReactJS applications hosted on app.mysite.com and profile.mysite.com and they are using JWT token for authentication and authorization process. now when I login into app.mysite.com through idp.com profile.mysite.com is un unauthenticated and needs another login. I use the same client configuration for both of these sites. I know there are some methods such as using an IFRAME inside client code to share the JWT token between these two app but I am looking for a built-in approach inside the Identity server to solve this issue?
First of all, if you have 2 CLIENTS, you should configure 2 separate configurations for both of them.
Afer separation of clients you should rely on cookie set on idp.com after first authentication. (Good to know - How to setup cookie authentication basic cookie authentication: https://learn.microsoft.com/pl-pl/aspnet/core/security/authentication/cookie?view=aspnetcore-6.0)
Anyway, if you configured IdentityServer properly, it handles cookie authentication "out-of-the-box" - so probably the only thing you have to do is to Signin the user.
AuthenticationProperties props = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.Add(LoginOptions.RememberMeLoginDuration)
};
var issuer = new IdentityServerUser(user.SubjectId)
{
DisplayName = user.Username
};
await HttpContext.SignInAsync(issuer, props);
When the youser want to login to second application, after start of the flow (eg. code flow) and redirect to the idp.com, idp.com knows that the user is already signed-in (cookie) and should immediately generate token and redirect back to the return url.
If you need you can adjust custom behaviours using IProfileService.

IdentityModel implementation: How to verify (and refresh) access_token on or after expiry date?

Backstory: I'm trying to link up a new client application using .NET 5 (MVC) with an existing IdentityServer4. IdentityServer4 (IS4 in short) is used both to authenticate the client, as well as providing claims & roles and the access_token that the API (separate webapp) depends on to request data on the backend systems. On the new client I'm using the IdentityModel package to handle the authentication and authorization. So far I've managed have both authentication and authorization working, but I'm running into issues regarding the expiration date of the access_token.
I've currently configured the client in IS4 to have the following. If the line says '(default)', it means that it's the defaults as suggested or set by IdentityModel/IS4:
IdentityTokenLifetime: 300s / 5m (default)
IdentityAccessToken: 300s / 5m (shortened to allow me to test)
AuthorizationCodeLifetime: 300s / 5m (default)
Flow
First example:
User browses to webpage, and get's redirected to IS4 to log in.
User fills in user/pass and successfully authenticates, and get's redirected back to the secure section of the web app.
When user hits the secure webpage, an api request is done to an external api using the users' access_token to grab the users' data.
Request comes back with users' data, and webpage with that data.
Working perfectly.
Second example:
User browses to webpage, and get's redirected to IS4 to log in.
User is already authenticated with IS4 using cookie, thus successfully authenticates, and get's redirected back to the secure section of the web app.
When user hits the secure webpage, an api request is done to an external api using the users' access_token to grab the users' data.
Request comes back with users' data, and webpage with that data.
Working perfectly.
Third example:
User waits 15 minutes on the webpage, and then refreshes the page.
User is already logged into the website, so no redirect to IS4 is happening.
Since user refreshes, the user hits the secure webpage, an api request is done to an external api using the users' access_token to grab the users' data.
Request comes back empty, as the access_token has expired (10 minutes prior)
Sad smiley :'(
Fourth example:
Following example three: User sees error and restarts browser.
User browses to webpage, and get's redirected to IS4 to log in.
User is already authenticated with IS4 using cookie, thus successfully authenticates, and get's redirected back to the secure section of the web app.
When user hits the secure webpage, an api request is done to an external api using the users' access_token to grab the users' data.
Request comes back with users' data, and webpage with that data. (because access_token is newly generated with future expiry date due to new IS4 'login')
Working perfectly.
Example three is the problem I'm having. What I expect to happen is that the [Authorization]-check doesn't allow expired sessions (access_tokens) to pass through, and instead redirect the user to IS4 to re-authenticate automatically based on the valid cookie the user still has (like example four).
What I've tried to fix:
Lengthening the IdentityAccessToken lifetime: Doesn't fix the problem, instead just moves the problem to the new expire_date.
Using the IdentityModel client "Web5" example on our existing IS4 implementation, which is showing the same behavior.
--
The requirements of the application are to have a short access_token lifetime to allow quick updates of users' right and access based on changed claims/roles in the backend, meanwhile allowing for 'persistent' logins to reduce the amount of time users' spend having to fill their accoutn details in.
It is entirely possible that instead of a technical problem my thoughtprocess or understanding of these things is wrong. If so, please enlighten me as to what the flow should be, preferably with a working example.
--
IdentityModel in the client is configured as following:
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services
.AddAuthentication(options => {
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.Events.OnSigningOut = async e =>
{
// revoke refresh token on sign-out
await e.HttpContext.RevokeUserRefreshTokenAsync();
};
})
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => {
options.GetClaimsFromUserInfoEndpoint = true;
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = Configuration.GetValue<string>("IdentityServer:Authority");
options.ClientId = Configuration.GetValue<string>("IdentityServer:ClientId");
options.ClientSecret = Configuration.GetValue<string>("IdentityServer:ClientSecret");
options.RequireHttpsMetadata = Configuration.GetValue<bool>("IdentityServer:RequireHttpsMetadata");
options.UsePkce = true;
options.ResponseType = OidcConstants.ResponseTypes.CodeIdToken;
options.SaveTokens = true;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = JwtClaimTypes.Name,
RoleClaimType = JwtClaimTypes.Role
};
// Scopes
options.Scope.Add("openid");
options.Scope.Add("offline_access");
})
.AddOpenIdConnect("persistent", options => {
options.CallbackPath = "/signin-persistent";
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = context =>
{
context.ProtocolMessage.Prompt = OidcConstants.PromptModes.None;
return Task.FromResult<object>(null);
},
OnMessageReceived = context => {
if (string.Equals(context.ProtocolMessage.Error, "login_required", StringComparison.Ordinal))
{
context.HandleResponse();
context.Response.Redirect("/");
}
return Task.FromResult<object>(null);
}
};
...
// Rest of 'persistent' is similar as the non-persistent one
...
});
// Examples of IdentityModel suggest that calling this function make the boilerplate tasks of refreshing tokens and alike automatically work
services.AddAccessTokenManagement();
For this flow, with a backend application the solution is the use refresh tokens which can be obtained by requesting the offline_access scope and ensuring the client is configured to allow them.
The refresh token is returned alongside the access token and can be used to get a fresh access token (via a back channel token endpoint call) once the initial one expires. This can either be done on first failure (i.e. a 401 response from the API) or based on the expiry time of the access token (either by using the expires_in token endpoint response value or the exp claim in the access token itself.
Check out: https://identityserver4.readthedocs.io/en/latest/topics/refresh_tokens.html
And the sample: https://github.com/IdentityServer/IdentityServer4/tree/main/samples/Clients/src/MvcAutomaticTokenManagement

AspNet Core 2.1 Identity - External login user log out not persisting [duplicate]

I have a WebAPI with OAuth login configured like this:
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
PostLogoutRedirectUri = "https://www.microsoft.com/",
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = context =>
{
context.HandleResponse();
context.Response.Redirect("/Error?message=" + context.Exception.Message);
return Task.FromResult(0);
}
}
});
and Login enforced for all Controllers using
config.Filters.Add(new System.Web.Http.AuthorizeAttribute());
I now want to add an ApiController called LogoutController (guess what it does).
I have found that I can logout from MVC using
System.Web.Security.FormsAuthentication.SignOut();
but I am not logged out from WebAPI that way. I have not found any information how to logout from WebAPI. But I have found that there may be a bug in logout procedure, the cookie is kept and has to be removed manually, but then, the code is MVC again, and it seems as if I can't get a HttpCookie into my HttpResponseMessage object:
[HttpGet]
public HttpResponseMessage Logout()
{
FormsAuthentication.SignOut();
// clear authentication cookie
HttpCookie cookie1 = new HttpCookie(FormsAuthentication.FormsCookieName, "");
cookie1.Expires = DateTime.Now.AddYears(-1);
var response = Request.CreateResponse(HttpStatusCode.OK);
response.Content = new StringContent("<html><title>Logout successful</title><body style=\"font-family:sans-serif\"><div style=\"display:table; width:100%; height:100%; margin:0; padding:0; \"><div style=\"display:table-cell; vertical-align:middle; text-align:center;\">You have been successfully logged out.<br>You can close this window/tab now.</div></div></body></html>");
response.Headers.AddCookies(cookie1); // Types don't match
return response;
}
How can I achieve that my WebAPI is logged out and does require OAuth to be done again before I am logged in?
You can't logout of the API because you're not logged in to it!
For example, say your API uses Facebook as its OpenID authentication provider.
Your user will have to log into facebook to use your API. Your API will redirect them to facebook auth server and if they are not logged in - facebook will ask them to log in.
If the user decides to stay logged into facebook, then each time they use your API, they will not be required to login to facebook again and your middleware code will obtain a valid token for them to access your API.
Your API can't remove the browser cookie between facebook and your user's browser so you can't log them out of facebook, so you can't stop them getting new tokens when they want.
I don't know what OpenID provider you use but I would think the above applies for any.
You can log out of MVC app as it would have created a cookie between you (user agent) and the MVC app when you logged in. It can delete its own cookie!
The easiest way is for the client itself to just "forget" the token - no need to tell server about it (this is what clearing the auth cookie really is doing - making the browser remove the cookie).
If you want the token itself to be no longer valid, than you would need to maintain a list of revoked tokens. For various reasons you may want your access tokens to be always valid but short lived and revoke refresh tokens instead.

Changing B2C Reply URL from "signin-oidc" to something else does not work

I am trying to get a ASP.NET Core 2.1 web app running with Azure AD B2C.
I have this running after much reseach (and more trial and error) as the instructions found in the docs are a little off.
The sign-in/sign-up process works using the default scaffolding during the project setup wizard, plus the new Microsoft.AspNetCore.Authentication.AzureADB2C.UI Nuget, which resulted in a simplified, yet "back boxed" experience during startup.
The problem currently is that I am unable to make this work with a custom Reply URL that is different from "signin-oidc". I have read that "signin-oidc" is baked into the provider somehow, and is hence the default.
I have a OnboardingController with a Start action defined where I want the user to land after signing up, so I have done the following:
A) I tested that the Url localhost:12345/Onboarding/Start works. The
page is displayed correctly.
B) In appsettings.json I change AzureAdB2C's "CallbackPath": "/signin-oidc" to "CallbackPath": "/Onboarding/Start"
C) I go to the tenant and change the application's Reply URL to localhost:12345/Onboarding/Start. IMPORTANT side note: Unlike in the ADB2C samples and guides, you MUST append the Reply URL with either signon-oidc or a custom request path! Localhost:12345 is NOT ENOUGH!
I can confirm the authentication worked:
Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler:Information: AuthenticationScheme: AzureADB2CCookie signed in.
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 56.201ms
Then, when I manually navigate to /Onboarding/Start in the browser, I get
Error from RemoteAuthentication: Correlation failed.
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 GET http://localhost:12345/Onboarding/start
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler:Warning: .AspNetCore.Correlation. state property not found.
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler:Information: Error from RemoteAuthentication: Correlation failed..
1.
But I am never redirected to the intendend /Start page. instead I land back on the root homepage, just like what happens when I used 'signin-oidc'. Why is that and how do I make it stop going there?
2. How can I have the Reply URL be different depending on whether you sign IN or UP? I can't really use a sign-up policy for one and a separate sign-in policy for the other, because the reply URL is identical.
I am new to .Net Core and I am at a loss as to how to even debug this. Signing in with B2C seems to be an obscure process. Any insight would be greatly appreciated.
EDIT:
This is the custom account controller, which is not built into the Nuget package for AzureADB2C.
While the Nuget package provides an internal AccountController, it does not allow you to set a custom Reply URL. Yet using my own account controller does not work for me. I am also not getting the "Correlation failed" error either, instead I get no error at all.
[Route( "[controller]/[action]" )]
public class AccountController : Controller
{
private readonly AzureADB2COptions azureAdB2COptions;
private const string PolicyAuthenticationProperty = "Policy";
private string scheme = AzureADB2CDefaults.AuthenticationScheme;
public AccountController( IOptions<AzureADB2COptions> b2cOptions )
{
azureAdB2COptions = b2cOptions.Value;
}
[HttpGet]
[Route( "/[controller]/SignIn" )]
public IActionResult SignIn()
{
var callbackUrl = Url.Action( nameof( OnboardingController.Start ), "Onboarding", values: null, protocol: Request.Scheme );
var properties = new AuthenticationProperties { RedirectUri = callbackUrl };
properties.Items[PolicyAuthenticationProperty] = azureAdB2COptions.SignUpSignInPolicyId;
return this.Challenge( properties, scheme );
}
The startup.cs code for B2C is unchanged from what the default .NETCore 2.1 template generated for me:
services.AddAuthentication( AzureADB2CDefaults.AuthenticationScheme )
.AddAzureADB2C( options =>
{
Configuration.Bind( "AzureAdB2C", options );
} );
An authentication request that is passed from your web application to Azure AD B2C can contain two redirect URLs:
One (often known as the reply URL) that is passed in the "redirect_uri" parameter, which must be registered with Azure AD B2C, to which all authentication responses are returned from Azure AD B2C to your web application. The default for this is /signin-oidc.
Another (often known as the return URL) that is round-tripped in the "state" parameter, which doesn't have to be registered with Azure AD B2C, to which the end user is returned after your web application has handled the authentication response. An example of this is /Onboarding/Start.
Your web application can set the return URL as follows:
public class AccountController : Controller
{
public IActionResult SignUp()
{
return this.Challenge(
new AuthenticationProperties()
{
RedirectUri = Url.Action("Start", "Onboarding", values: null, protocol: Request.Scheme)
},
AzureADB2CDefaults.AuthenticationScheme);
}
}
The ChallengeResult object creates an authentication challenge for the Azure AD B2C authentication middleware that is added by the AzureADB2CAuthenticationBuilderExtensions.AddAzureADB2C method.
The first argument to the ChallengeResult constructor invokes the OpenID Connect authentication handler that is registered by the Azure AD B2C authentication middleware.
The second argument to this constructor sets the return URL to which the end user will be returned after the Azure AD B2C authentication middleware has handled the authentication response.
It is my understanding that you will need to enable the the Application Claim -> newUser.
This flag is only set to true when a user initially signs up. Once they are redirected back to your website, you will need to read the claim and redirect to your onboarding/start or other controller if they are an existing user.
You can change the redirect_uri to any action in your website, but the problem is that the uri is only used as an endpoint. The action at the uri is never executed. So I think it will be very hard, if not impossible, to change the routing at the redirect_uri.
But I found another solution to make sure your Onboarding Registration is executed, using the Authorize attribute with a policy like: [Authorize(Policy="HasUserId")]
Take a look at https://stackoverflow.com/a/57672145
But I am never redirected to the intendend /Start page. instead I land back on the root homepage, just like what happens when I used
'signin-oidc'. Why is that and how do I make it stop going there?
How can I have the Reply URL be different depending on whether you sign IN or UP? I can't really use a sign-up policy for one and a
separate sign-in policy for the other, because the reply URL is
identical.
I was facing very similar issue to yours. You can have a look at this answer
which says
"The CallbackPath is the path where server will redirect during authentication. It's automatically handled by the OIDC middleware itself, that means we can't control the logic by creating a new controller/action and set CallbackPath to it . Below is the general process :
During authentication, the whole process is controlled by OpenID Connect middleware , after user validate credential in Azure's login page ,Azure Ad will redirect user back to your application's redirect url which is set in OIDC's configuration , so that you can get the authorization code(if using code flow) and complete the authentication process . After authentication , user will then be redirected to the redirect URL ."
Here you can find an example how I managed to handle redirects to different routes after login via AzureAd.

ASP.NET Identity: Not updating cookie after modifying claims

I'm having issues with updating claims on ASP.Net Identity 2.2.1 in .Net 4.6.2 / MVC5.
After updating a claim it will normally send an updated cookie to the browser and everything works fine but sometimes no set cookie header is sent to the browser.
I've not been able to identify any pattern as to when it happens other than when it is failing, the server is sending a
Persistent-Auth: true
http header value for every response in the session. I don't know what causes this header value to get set and it sometimes appears mid-session and once it starts sending it, it will be sent for the rest of the session and trying to update the claims will never work again for that session.
As far as I can see, I have hard-coded the isPersistent parameter to be false in every call into ASP.Net identity and I can't see anything else that could be related to this header.
The code I'm using for updating claims is
public static void UpdateClaims(List<Claim> claims)
{
var authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
var newIdentity = new ClaimsIdentity(HttpContext.Current.User.Identity);
foreach (Claim claim in claims)
{
Claim oldClaim = newIdentity.FindFirst(claim.Type);
if (oldClaim != null && oldClaim.Type != "")
{
newIdentity.RemoveClaim(oldClaim);
}
newIdentity.AddClaim(claim);
}
authenticationManager.AuthenticationResponseGrant = new AuthenticationResponseGrant
(new ClaimsPrincipal(newIdentity), new AuthenticationProperties { IsPersistent = false });
}
This is being called from an MVC action method.
Does anyone have any suggestions what might be going wrong or even just a starting point of where to look? I don't know what causes that persistent-auth header but it looks to be related to the problem; whether it is the cause or a symptom of the problem, I don't know.
I'm using ASP.Net Identity 2.2.1 with .Net 4.6.2.
I'm running on Windows Server 2012R2 and the problem seems to occur with IE11, Chrome and Firefox.
I'm using Fiddler 4.6.3 to view the http headers / responses.
Update:
I have noticed that it seems to go wrong only when Windows authentication is enabled. My server has a setting that allows username/password, windows auth or both (user can choose to sign in as a different user using username/password). When windows auth is used, I initially authenticate the user using windows and then set a cookie, which I then use for all future requests in the session.
If windows auth is disabled, updating the claims like this always works. If windows auth is enabled, updating the claims usually works.
First, you're conflating two different things, although it's understandable since they're named similarly. The IsPeristent setting determines whether the cookie is a session-cookie or persistent cookie. In other words: it determines whether or not the cookie will expire when the browser is closed or at some predetermined time, whether or not the browser is closed.
The Persistent-Auth header is an optimization header that informs the client that it doesn't necessarily need to authorize each request. It has nothing to do with the IsPersistent flag.
Claims are set at login. Period. If you need to update the claims, you must sign the user out and sign them back in. This can be done programmatically (i.e. without user intervention), but it must be done. In other words, if you need to alter a claim, and you need that alteration to be available in the next request, then you follow it with:
Identity 2.0
AuthenticationManager.SignOut();
await SignInManager.SignInAsync(user);
Identity 3.0
await SignInManager.RefreshSignInAsync(user);
Instead of
authenticationManager.AuthenticationResponseGrant =
new AuthenticationResponseGrant(new ClaimsPrincipal(newIdentity),
new AuthenticationProperties { IsPersistent = false });
you should use
authenticationManager.SignIn(
new AuthenticationProperties { IsPersistent = false },
new ClaimsPrincipal(newIdentity));
I found the problem. It was using the wrong identity when it tried to update the claims. In my scenario there were two identity objects, one for windows authentication and one for cookie authentication. In most cases HttpContext.Current.User.Identity gets the cookie authentication object (which is the one with the claims) but occasionally it was giving me the windows authentication object, so when I tried to update the claims on that, it didn't do anything.
The problem was solved by replacing
var newIdentity = new ClaimsIdentity(HttpContext.Current.User.Identity);
with
ClaimsIdentity oldIdentity = claimsPrincipal.Identities.FirstOrDefault(i => i.AuthenticationType == "ApplicationCookie");
var newIdentity = new ClaimsIdentity(oldIdentity);
It now seems to work solidly without needing to sign out / back in again.
I guess the Persistent-Auth: true http header was being sent when OWin considered the Windows auth to be the primary identity so that is why its presence correlated with the inability to update claims.
I believe in our case it was the wrong/long-term IoC lifetime/'scope' of the ApplicationUserManager & possibly the same for the role manager too.

Categories