IdentityServer4 PostLogoutRedirectUri - identityserver4

I am confused about how this is used.
Most examples I've seen have it given as "/signout-callback-oidc". That seems to indicate that it uses OIDC middleware in the process. What if I want to return to a specific client page?
The automatic redirect isn't working when I set IdentityServer's AccountOptions.cs property of AutomaticRedirectAfterSignOut to true. Further, during logout, I do not receive the client's PostLogoutRedirectUri.
So, is that link supposed to go to the OIDC middleware, or is it available for use to redirect to the client?

Your client has to be configured to request the callback to one of those URIs as part of the client-initiated sign-out flow.
IS4 clients can be configured with lists of allowable redirect URIs for both sign-in and sign-out, which I'm guessing is where you see /signout-callback-oidc -- if I remember right, either the docs or maybe the Quickstart code uses that, but there's nothing special about that particular URI name. (It isn't some OIDC standard, or a "well-known" name, or anything of that nature, as far as I know.)
The missing piece of the puzzle is to configure OIDC in the client application. You didn't mention what kind of application is on the client side, but in ASP.NET Core it's an option named SignedOutCallbackPath on the AddOpenIdConnect service:
services.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = appConfig["OidcAuthority"];
options.ClientId = appConfig["OidcClientId"];
options.ClientSecret = appConfig["OidcClientSecret"];
// etc
options.SignedOutCallbackPath = "/jake-says-goodbye";
});
This causes the OIDC implementation to add a property to the sign-out request identifying that redirect URI. As long as your application properly identifies itself, as briefly mentioned in the docs here, and as long as /jake-says-goodbye is one of the approved post-logout redirect URIs on the IS4 side, you should get the callback you're expecting.
(I specifically mention "proper" identification because, based on github questions I've seen, it sounds like it might be more difficult to manage for a JS-based SPA client app versus whatever helpful things MVC does behind the scenes to manage server-to-server OIDC interaction. I can't speak to that as I've not had a need to implement any SPA clients with IS4 yet.)

The problem is that you have to set a very specific parameter in order for the PostLogoutRedirectUri to not show up as null on IdentityServer's side, and testing any of the options results in having to step through a ton of ways to set it, most of them still resulting in null. Since I'm using an older client with IdentityServer4 (in order to enable .NET 4.x webapps to authenticate through IdentityServer4, cannot easily use .NET Core with those projects - luckily IdentityServer4 is still compatible with the older client code), the action that triggers signout has two relevant things (and you'll find a TON of examples of code for that will not work for you with MVC in .NET 4.x):
Use the signout() method in this sample github repo (the IdentityServer3 MVC Owin sample client): https://github.com/IdentityServer/IdentityServer3.Samples/blob/master/source/Clients/MVC%20OWIN%20Client/Controllers/HomeController.cs You can trigger that action from a button in a view.
That will get intercepted by the client's Owin middleware if you do this: https://github.com/IdentityServer/IdentityServer3/issues/2687#issuecomment-194739035 I didn't use the stored message bit, and I added the PostLogoutRedirectUri parameter in a way that IdentityServer4's LogoutRequest model wouldn't remove with this line in the same segment:
n.ProtocolMessage.PostLogoutRedirectUri = "http://myredirectaddress/ActionToDoOnceReturned";
You have to make sure that the above matches the client's PostLogoutRedirectUri on the IdentityServer side's client config or it'll be null again, and you would have missed it among all the other parameters. For instance, these methods of setting PostLogoutRedirectUri DO NOT work:
n.ProtocolMessage.SetParameter("PostLogoutRedirectURI", "some URL");
n.ProtocolMessage.SetParameter("PostLogoutUri", "another URL");
n.ProtocolMessage.SetParameter("PostLogoutRedirectUri", "yet another URL that's going to be ignored by IdentityServer4");
From there, you're off to the races because PostLogoutRedirectUri is no longer null. There are a few more considerations: check out AccountOptions in the IdentityServer controller folder. I set AutomaticRedirectAfterSignout to true there (this is used by Javascript in IdSrv's final logout page - if set, the script uses PostLogoutRedirectUri to forward the user back to the client). There's also an option to show a logout confirmation prompt, which if you want to actually display, make sure to NOT set the id token hint in the Owin (it's right next to where we set the PostLogoutRedirectUri / the part that gets triggered by signout requests). If you do those two things, AccountServices.BuildLogoutViewModel will return the prompt to the user when it's called by the AccountController.logout() method.
Thank you aaronR for the answer to my other question concerning that part:
IdentityServer4 logout (id token hint tells IdentityServer that the signout request was authorized and not a malicious person trying to harass your system / sign out users, IdSrv will ask the user for confirmation if it's not provided).
Finally, if you are confused by what's happening on the IdentityServer side in logout, and why it's repeatedly triggering the same method:
First time it gets called from the client's Owin middleware (the bit of code above that gets triggered after the Signout() action).
It uses AccountService to build a view model to return to the user for logout confirmation.
It gets triggered again by the user clicking yes on that page.
It goes through the Account service method again, which this time sets the bool to show the logout confirmation to false.
It calls the second logout method, the one with the view model that gets passed in.
This one triggers the external identity provider signout.
The external identity provider returns control back to logout, resulting in it getting called again, calling the second logout method again.
Finally, it will return the user to IdentityServer's logout page. If PostLogoutRedirectUri is set & AutomaticRedirectAfterSignOut is true, there's javascript on that page which automatically forwards the user's browser to it.
Due to having two projects to debug through at once and all of these possible ways of setting the redirect (which also have to match client/server side config in order not to be null) it can be easy to get confused.

Overview
When setting up IdentityServer (assuming it's a separate application), there are two parameters in the configuration for an accessing client: RedirectUris and PostLogoutRedirectUris. These correspond to what happens after a login or logout of a user against the IdentityServer system.
Since your client app probably has its own cookies (or JWT tokens, or whatever it's using to maintain a user session), it needs to know when the IdentityServer has processed the login and made the user data available.
The default ASP.NET OpenID Connect middleware does this with yourapp.com/signin-oidc and yourapp.com/signout-callback-oidc endpoints to intercept and handle the login/logout hand-off from IdentityServer. These endpoints have nothing to do with the OpenID protocol and can be set to whatever you want if you have your own authentication handler, but if you're using the default middleware then you must set them to that in the IdentityServer config.
Config option
However, if you still want to redirect a user after the OpenID Connect logout has completed, there's an option specifically for this:
services.AddOpenIdConnect(options =>
{
// your other options...
options.SignedOutRedirectUri = "/some-page-after-oidc-logout";
});
Microsoft Docs

I want to share how I solved problem with null PostLogoutRedirectUri value.
I always had null PostLogoutRedirectUri value in logout context until I added SignInScheme value on mvc client side.
These settings of authentication on MVC client side works for me:
var authenticationBuilder = services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
});
authenticationBuilder.AddCookie(options =>
{
options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
options.Cookie.Name = "identity_server_mvc";
});
authenticationBuilder.AddOpenIdConnect("oidc", options =>
{
options.Authority = "{IDENTITY_SERVER_URL}";
options.ClientId = "mvc";
options.SaveTokens = true;
options.SignInScheme = "Cookies";
});
You also need to make sure that you have added the PostLogoutRedirectUri value to the client configuration on the Identity Server side:
new Client
{
ClientId = "mvc",
AllowedGrantTypes = GrantTypes.Implicit,
RedirectUris = { "{CLIENT_URL}/signin-oidc" },
PostLogoutRedirectUris = { "{CLIENT_URL}/signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
}
}
One last but important point, because of this I had null logoutId value on Identity Server side. To initiate Logout process you must first call SignOut("Cookies", "oidc") on mvc client side. Example endpoint in my HomeController:
public IActionResult Logout()
{
return SignOut("Cookies", "oidc");
}
Good luck!

Building on top of #McGuireV10 answer if your project is a Blazor WASM, then the change would be like this:
// Adds OpenID Connect Authentication
builder.Services.AddOidcAuthentication(options =>
{
options.ProviderOptions.Authority = settings.Authentication.Authority;
options.ProviderOptions.ClientId = settings.Authentication.ClientId;
options.ProviderOptions.ResponseType = "code";
options.ProviderOptions.ResponseMode = "query";
//
options.AuthenticationPaths.LogOutCallbackPath = "authentication/logout-callback";
builder.Configuration.Bind("oidc", options.ProviderOptions);
});

I ran into the same issue today; your (#JakeJ) link solved it for me. I am building a demo MVC Owin Client in .net 4.6.1 (for a third party company) so our set up is the same and our Id Svr v4 is built on net core v3.1.
I verified i had the same PostLogoutRedirectUri defined in the Id Svr side config for the client i was working on and then at the client side config too.
But i noticed that i could add a small block of code taken from the ref'ed github issue to the RedirectToIdentityProvider func delegate specific to logout.
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
{
// below is technically not needed, as it was already set for me.
n.ProtocolMessage.PostLogoutRedirectUri = LoginAndOutRedirectUri;
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token").Value;
n.ProtocolMessage.IdTokenHint = idTokenHint;
}
This means that a claim needs to be present in order for this to work so i then added the below to the SecurityTokenValidated func delegate:
// add id token for logout
currentIdentity.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
I saw many examples where folks were populating the AuthenticationTicket inside the AuthorizationCodeReceived func delegate but for me it was always null. So some head scratching lead me to implementing what i needed inside the SecurityTokenValidated delegate. And it all works and hands together nicely.

Related

How do you handle authentication and token refresh with Microsoft Identity and Azure AD

I'm attempting to secure a .Net 6.0 / Razor Page web application against Azure AD. I was able to complete the application registration with Azure AD and successfully authenticate users. The issue I'm facing occurs when the issued token expires. I have some experience working with Angular and IdentityServer implementations, but Razor Page/Microsoft Identity is still new to me.
What I would like to happen:
The user logs in with their Microsoft account
The user's session is uninterrupted for up to 12 hours (with all token management happening behind the scenes)
After 12 hours the session/cookies will expire and the user will need to log in again
What is happening:
The user logs in and is authenticated
After approximately one hour, the application triggers a call to the /authorize endpoint the next time the user takes any action (such as trying to navigate to a new page)
This causes the application to reload on the page the user was currently on (thus interrupting their experience)
Additional Issue: I am also receiving a CORS error under similar circumstances as above. The difference here is this is occurring when the user is in the middle of form data entry when the (presumed) token expiration occurs. When they click submit to post the form, a 302 xhr / Redirect to the /authorize endpoint is triggered. This call results in a CORS error. Refreshing the page is required to trigger a successful call (and they need to start over on their form). Update: This is occurring due to an AJAX call (nothing to do with the form/post specifically). See the edit at the end.
Ideally, I would like the token to be automatically (and silently) refreshed via a refresh token once it is nearing expiration. I would also, of course, like to avoid the scenario of the CORS error when they are attempting to post when the token has expired.
Some code snippets (note: I'm manually adding authentication to an existing app, I did not use any scaffolding/templates for the initial project creation).
Note: I initially tried the below implementation without defining custom authOptions, but during debugging and different attempts at resolution, it exists in the below state. Results were consistent either way.
Program.cs
var builder = WebApplication.CreateBuilder(args);
var config = builder.Configuration;
var services = builder.Services;
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(
authOptions =>
{
config.Bind("AzureAD", authOptions);
authOptions.MaxAge = TimeSpan.FromHours(12);
authOptions.SaveTokens = true;
},
sessionOptions =>
{
sessionOptions.Cookie.MaxAge = TimeSpan.FromHours(12);
sessionOptions.Cookie.Name = "Custom-Cookie-Name";
sessionOptions.ExpireTimeSpan = TimeSpan.FromHours(12);
sessionOptions.SlidingExpiration = false;
})
.EnableTokenAcquisitionToCallDownstreamApi(config.GetValue<string>("GraphApi:Scopes")?.Split(' '))
.AddMicrosoftGraph(config.GetSection("GraphApi"))
.AddSessionTokenCaches();
services.AddRazorPages(options =>
{
options.Conventions.AddPageRoute("/Disclaimer", "/");
})
.AddMvcOptions(options =>
{
var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
services.AddHttpContextAccessor();
........
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseSession();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapRazorPages();
});
app.UseSaveUserDetailsOnAuthentication();
app.UseIdentityPageInitialization();
app.MapRazorPages();
app.MapControllers();
app.Run();
I also have some middleware that is using the graph service to hit the /me endpoint and store some user details under specific conditions (in case this is relevant):
Graph Middleware
public async Task InvokeAsync(HttpContext context, UserManager<ApplicationUser> userManager, GraphServiceClient graphServiceClient)
{
var page = context.GetRouteValue("page")?.ToString();
if (!page.IsNullOrEmpty() && page.Equals("/Disclaimer") && context.User.Identity?.IsAuthenticated == true)
{
var user = await graphServiceClient.Me
.Request()
.GetAsync()
.ConfigureAwait(false);
The below snippet is what occurs when attempting the post scenario above.
The tl/dr questions are, using the Microsoft Identity libray/MSAL, how do I:
Silently refresh a user's token
Avoid reloading the page to get a new token (i.e.: calling /authorize and redirecting to obtain a new token)
Handle token expiration from the client-side (avoid the CORS error when posting a form). Do I need to add an additionally client-side js library to manage this?
I've tried scouring Microsoft's documentation, but nothing I've found goes into detail on this. The closest I found was MSAL's documentation mentioning that it handles token refresh for you (but it seemingly isn't happening in my case).
I'm expecting that the token will be silently refreshed by the underlying MSAL library, but that does not appear to be happening. Additionally, I'm expecting to avoid CORS errors on the front-end related to token expiration.
EDIT: While my main question still remains, I believe I found the resolution for the secondary issue: the CORS issue which is actually triggered via an AJAX call to the API. This article outlines that Microsoft.Identity.Web v1.2.0+ now handles this scenario. I now have a vague idea on how to handle it, but still need to attempt the implementation.
I found a reference here explaining that these session token caches have a scoped lifetime and should not be used when TokenAcquisition is used as a singleton, which I believe is the case with the use of the Microsoft Graph API ("AddMicrosoftGraph").
I switched the session token cache to a distributed SQL token cache. However, I do not believe any of this was actually the root issue.
I've identified an issue causing my server (clustered behind a LB without sticky sessions) encryption keys to not be correctly stored/shared in a distributed store. What was happening is any idle timeout in ISS would reset them, causing the auth cookie to be unusable. Additionally, any time the app would hit a different web server behind the LB, the existing auth cookie to be unusable by the new server (because they were using separate keys). So in both scenarios the application would redirect the user for authentication.
The fix for this was simply implementing a distributed key store as described here. The provided stores did not work for me, due to restrictions put in place by my client, so I just implemented a custom IXmlRepository and registered it:
services.Configure<KeyManagementOptions>(options => options.XmlRepository = new CustomXmlRepository());
So at the end of the day I had the following issues:
The auth cookie was becoming invalidated due to changing/lost keys as described above: Resolved by adding a distributed key store
The Microsoft GraphServiceClient was unable to obtain access tokens/refresh tokens (resulting in MSAL errors), due to a lack of a distributed token store as well as due to changing/lost keys (when I was storing tokens in the cookies): Resolved by adding a distributed token store (described here)

Co-hosting Identity Server 4 with API services using Roles

I've come across an example of co-hosting Identity Server 4 on the same App Host as the API Services that need it for authentication and authorization.
Now I was able to replicate this successfully by just pure authentication but when it comes to authorization using Roles I couldn't get it to work, i.e. adding the [Authorize(Roles = "My Role")] attribute on my Controller Action.
The access token contains the "role" scope and claim but it doesn't seem to be respected at all.
I initially tried the code below but it doesn't execute the JWT Bearer bit at all which leads me to believe that Identity Server uses its own handler for that purpose and I have no idea how to configure it if at all.
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
// JWT tokens (default scheme)
.AddJwtBearer(options =>
{
options.Authority = "https://localhost:44367/";
options.Audience = "API";
options.TokenValidationParameters.RoleClaimType = "role";
});
Then I came across this line from the example code (I mentioned initially in this post) which seems like its supposed to grant me the ability to run API services along side with Identity Server:
services.AddLocalApiAuthentication();
But it also doesn't seem to do what I want.
So does the Identity Server authentication middleware allow me to accomplish role-based authentication or is there some other mechanism (i.e. Policies) that I need to look into?
Something worth noting, I was able to accomplish all of this successfully with Identity Server 4 but by hosting it all separately. I want to see what it takes to host it all together.
Just rechecked the example and it worked perfectly fine.
When you use
services.AddLocalApiAuthentication();
it sets up the IdentityServerAccessToken authenticationScheme.
To use it in your API controller you type
[Authorize(IdentityServerConstants.LocalApi.PolicyName)]
as described in the doc, or just
[Authorize(AuthenticationSchemes = "IdentityServerAccessToken")]
All you need to check the roles is one more argument for the attribute:
[Authorize(AuthenticationSchemes = "IdentityServerAccessToken", Roles = "test role1")]

How do you create a API/IdentityServer/Blazor(server-side) application?

I attempted to build this application myself but, have hit several stumbling blocks along the way. I am thinking that it may be best to step back and take a larger look at what I am trying to create. There doesn't seem to be any documentation on how to make what I am looking for. (unless someone can point me in the right place I might have missed)
Ultimately what I would like is to have a Blazor(server-side) application make API calls to use data in the app and then have an IdentityServer4 encapsulate the authentication. I need to have Azure as well as ASP.net Identity as the possible authentication methods.
I have tried and was able to create an IdentityServer4 that also has a local API. I can make calls to this from Postman to get token and such. But, when it comes to tying a Blazor(server-side) application to the IdentityServer4 I am befuddled.
I have tried to ask this question in specifics but, haven't gotten any results at all. I am hoping maybe this larger look at it might be helpful.
It seems like odic-client.js is the way to get the data from the IdentityServer4 callback but, that doesn't seem to tie in nicely with the .NET Authorization in Blazor(server-side). How do I get these to work together.
IMPORTANT: There are better sources now than my answer. Follow the links provided in the last part of this answer.
I've got a similar setup with API / IdentityServer4 / Blazor(server-side). I'll show you some of the code I used, maybe you can make some use of it.
Using the NuGet Package Microsoft.AspNetCore.Authentication.OpenIdConnect, I've got this code in the ConfigureServices method in the Startup class:
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://localhost:5001";
options.ClientId = "myClient";
options.ClientSecret = "mySecret";
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("MyApi");
options.Scope.Add("offline_access");
options.ClaimActions.MapJsonKey("website", "website");
});
and in the Configure method app.UseAuthentication();
Then in App.razor i used the CascadingAuthenticationState component:
<CascadingAuthenticationState>
<Router AppAssembly="typeof(Startup).Assembly" />
</CascadingAuthenticationState>
And using the NuGet package Microsoft.AspNetCore.Authorization in my main page Index.razor:
#using Microsoft.AspNetCore.Authorization
#attribute [Authorize]
Now it should say "Not authenticated" when you open the main page but there's still no redirection to the IdentityServer4. For this you've got to add MVC in the startup too, as I learned from this stackoverflow question:
services.AddMvcCore(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
Now you should be getting redirected to IdentityServer4 to log in after starting the application. In my case I've got an ApiClient, which describes the methods of my API. I use DI to inject the ApiClient and add the access token:
services.AddHttpClient<IApiClient, ApiClient>(async (serviceProvider, client) =>
{
var httpContextAccessor = serviceProvider.GetService<IHttpContextAccessor>();
var accessToken = await httpContextAccessor.HttpContext.GetTokenAsync("access_token");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
client.BaseAddress = new Uri("http://localhost:55578");
});
Like you said, there is not much documentation on this topic except some answers here on stackoverflow. It took me a long time to set this up, so I hope I can help someone else with this post.
UPDATE: Logout process
Logging out with this setup requires a detour to a razor page because the HttpContext is inaccessible after the blazor component is loaded.
Create a new Razor Page in the Pages folder and add the following code to the newly created Logout.cshtml.cs:
public class LogoutModel : PageModel
{
public async void OnGetAsync()
{
await HttpContext.SignOutAsync("Cookies");
var prop = new AuthenticationProperties()
{
RedirectUri = "http://localhost:62909"
};
await HttpContext.SignOutAsync("oidc", prop);
}
}
Add a logout button somewhere which calls the function UriHelper.NavigateTo("/Logout") relying on #inject IUriHelper UriHelper. Done!
UPDATE: Login Workaround
The previously described login process worked locally but after publishing to the test server, I had the problem, that the IHttpContextAccessor was always null inside the AddHttpClient method. So I ended up using the same workaround as with the logout process. I let the IdentityServer redirect to a razor page (which always has a HttpContext), save the access token in the user claim and redirect to the index page. In the AddHttpClient method I only get the token from the user claim and put it into the authentication header.
UPDATE: Open issues
I still struggle to get this setup working on our server. I opened this issue and requirement on the AspNetCore Github but both got closed without a proper answer. For the time being, I found some blogs that give a good overview of the current state of the topic:
https://mcguirev10.com/2019/12/15/blazor-authentication-with-openid-connect.html
https://wellsb.com/csharp/aspnet/blazor-consume-identityserver4-protected-api/
Try this
Blazor Consume IdentityServer4 Protected API

Blank page after login using bookmarked authorization URL in IdentityServer4

We have discovered that our users very often for the first time visits our web application by browsing the direct URL of the OIDC client (https://oidcclienturl.com/), The ASP.NET Core OIDC authentication middleware kicks in and the user gets redirected back to Identityserver 4 login page.
Everything works fine but then they decide to add the (temporary? state, nonce, cookies...) authorization URL as a bookmark in their browser before entering their credentials and continuing back to the web application.
This causes an issue when the user later uses the bookmark in a new session. The login seem to actually work after entering valid user credentials even if the user uses an old authorization URL, but when the user gets redirected back to the web application they end up on a blank page (https://oidcclienturl.com/signin-oidc).
After the blank page have been loaded the user is able to browse the direct URL (https://oidcclienturl.com/) sucessfully and appear as an authentcated user in the web application.
Any ideas whats causing the blank page?
That blank page shouldnt exist, if I understand it correctly its the default callback path of the oidc authentication middleware in ASP.NET Core.
Unfortunately, the real-world problem of users bookmarking the login page isn't handled cleanly by OIDC, which requires the client app to initiate the login flow.
I've addressed this by adding a RegistrationClientId column to my user data table, which is the Identity Server ClientId corresponding to the client app that called IDS when the user account was created. In the client app configuration, we use the custom Properties dictionary to add a URI fragment:
new Client
{
ClientId = "some_client",
ClientName = "Some Client",
ClientUri = "https://localhost:5000",
Properties = new Dictionary<string, string>
{
{ "StartLoginFragment", "/Auth/StartLogin" }
}
// other config omitted
};
When a user logs in, an empty return URL indicates IDS wasn't called by a client app, so we use RegistrationClientId to query IClientStore, then we combine the ClientUri and StartLoginFragment URIs and use the resulting URI to redirect the user back to the client application.
Over in the client application, that endpoint kicks off the OIDC sign-in flow, and since the user is already signed-in on IDS, it comes right back to the correct location in the client app. The controller action looks like this:
[HttpGet]
public async Task StartLogin()
{
await acctsvc.SignOutAsync();
await HttpContext.ChallengeAsync("oidc",
new AuthenticationProperties()
{
RedirectUri = "/"
});
}
The call to SignOutAsync just ensures any client-app signin cookies are cleaned up. It's in our custom account service, but it just runs HttpContext.SignOutAsync on the usual "Cookies" and "oidc" schemes. Normally that would also result in a signout call to IDS, but the redirection by the subsequent ChallengeAsync replaces the pending signout call.
The downside is that the action is an HTTP GET meaning pretty much anyone could theoretically trigger this action. At most it would be an annoyance.
In the special case where your IDS is only handling auth for a single client, you can skip a lot of that -- if they land on the page with no return URL, just send them to your client app start-login endpoint straightaway, before they login.

signin from external (but trusted) clients in identity server 4

I had a requirement that the user could enter his password and username directly on the client to sign on.
Without much trouble i just created a very simple extra authenticate action within the same application as identity server that looks like the following.
public async Task<IActionResult> AuthenticateUser(
[FromBody] LoginInputModel model,
[FromServices] AzureB2CUserService userService
)
{
var context = new ResourceOwnerPasswordValidationContext { Password = model.Password, UserName = model.Username };
await userService.ValidateAsync(context);
if (context.Result.Subject != null)
{
AuthenticationProperties props = null;
// only set explicit expiration here if persistent.
// otherwise we reply upon expiration configured in cookie middleware.
if (AccountOptions.AllowRememberLogin && model.RememberLogin)
{
props = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
};
};
// issue authentication cookie with subject ID and username
// var user = _users.FindByUsername(model.Username);
await HttpContext.SignInAsync(context.Result.Subject.GetSubjectId(), model.Username, props, context.Result.Subject.Claims.ToArray());
return Ok(context.Result);
}
ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage);
return BadRequest(ModelState);
}
from the single page application I then call this action and if success, i know that a local authentication cookie has been for identity server.
Then i do a .userManager.signinSilent() from the oidc-client and since the cookie is there, it will get a token exactly the same way if I had used an implicit grant with userManager.signInRedirect but without the user getting redirected.
Is there something I should be aware of from a security point here. (You may assume that cross site attacks and antiforgery tokens have been handled).
instead of callign the silent signin after, could I just do a redirect to the implicit flow in the custom authenticate method and have it end up again with the client application ?
Is there something I should be aware of from a security point here.
(You may assume that cross site attacks and antiforgery tokens have
been handled).
My understanding is that you (1) forfeit the ability to participate in Single Sign-on since your browser does not redirect to the SSO Authority, and (2) introduce a weakness in password handling since your client app (both JS/C# in this case) see the password in the plain text.
instead of callign the silent signin after, could I just do a redirect
to the implicit flow in the custom authenticate method and have it end
up again with the client application ?
If you did this, then you would essentially have the authorization_code flow without the authorization code. Might as well just upgrade to the higher security of that flow.
Assuming you have a SPA on top of an ASP.NET MVC app, you could use a traditional MVC form post with a redirect to SSO and then, upon return, spin up the SPA.

Resources