IdentityServer4 PostLogoutRedirectUri null - c#

I am attempting to get the implicit flow working for IdentityServer4. Login and logout work correctly, however the PostLogoutRedirectUri is coming back null, despite setting the value where it needs to be set. What I would like is for the logout process to redirect back to my application after the logout is complete.
I am getting the logoutId correctly, and Logout calls BuildLoggedOutViewModelAsync:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout(LogoutInputModel model)
{
var vm = await _account.BuildLoggedOutViewModelAsync(model.LogoutId);
...
This method is located in my AccountService.cs class, which then calls the GetLogoutContextAsync of the DefaultIdentityServiceInteractionService:
public async Task<LoggedOutViewModel> BuildLoggedOutViewModelAsync(string logoutId)
{
// get context information (client name, post logout redirect URI and iframe for federated signout)
var logout = await _interaction.GetLogoutContextAsync(logoutId);
...
Which creates a IdentityServer4.Models.LogoutRequest.
The SignOutIFrameUrl string property is set to "http://localhost:5000/connect/endsession/callback?sid=bf112f7785bc860fcc4351893012622e&logoutId=d6649e7f818d9709b2c0bc659696abdf" but nothing else seems to have been populated in the LogoutRequest.
Unfortunately, this means that the PostLogoutRedirectUri is null and the AutomaticRedirectAfterSignOut is also null, and when the LoggedOut.cshtml page is loaded, the signout-callback.js file is never loaded:
#section scripts
{
#if (Model.AutomaticRedirectAfterSignOut)
{
<script src="~/js/signout-redirect.js"></script>
}
}
Here are my configuration settings.
Config.cs:
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientId = "implicit.client",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"ContractManagerAPI"
},
RedirectUris = { "http://localhost:9000/" },
PostLogoutRedirectUris = { "http://localhost:9000/" },
AllowedCorsOrigins = { "http://localhost:9000" },
RequireConsent = false,
}
};
}
app.ts (js client):
import {UserManager} from 'oidc-client';
import { inject, Factory } from 'aurelia-framework';
#inject(Factory.of(UserManager))
export class App {
userManager: UserManager;
constructor(userManagerFactory){
let config = {
authority: 'http://localhost:5000',
client_id: 'implicit.client',
response_type: 'id_token token',
scope: 'openid profile ContractManagerAPI',
redirect_uri: 'http://localhost:9000/',
post_logout_redirect_uri: 'http://localhost:9000/'
};
this.userManager = userManagerFactory(config);
}
login(){
this.userManager.signinRedirect();
}
logout(){
this.userManager.signoutRedirect();
}
}
Relevant parts of Startup.cs:
services.AddIdentityServer()
.AddTemporarySigningCredential()
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddContractManagerUserStore()
.AddProfileService<ContractManagerProfileService>();
Any assistance in figuring out where I'm going wrong would be greatly appreciated.
Thanks!

pass id_token_hint arg to signoutRedirect()
you can get id_token_hint from the User object returned by signinRedirect();
so lets say you got a variable called "user" in your ts file that got set as a result of the user logging in via signinRedirect().
then you would do...
logout(){
this.userManager.signoutRedirect({ 'id_token_hint': this.user.id_token });
}

I was running into (possibly) the same issue as the OP.
After examining the logs from IdentityServer, I noticed that an exception was being thrown just after the request information containing my client information was sent to the IdentityServer server.
This lead me to this github post which addresses the error directly. The solution was to update to IdentityServer4 v4.1.2. I then re-ran my code and voila! the PostLogoutRedirectUri (and other parameters) were now correctly populated from the var context = await _interaction.GetLogoutContextAsync(logoutId); call.

Make sure you have these settings properly configured:
public class AccountOptions
{
public static bool AllowLocalLogin = true;
public static bool AllowRememberLogin = true;
public static TimeSpan RememberMeLoginDuration = TimeSpan.FromDays(30);
public static bool ShowLogoutPrompt = false;
public static bool AutomaticRedirectAfterSignOut = true;
public static bool WindowsAuthenticationEnabled = false;
// specify the Windows authentication schemes you want to use for authentication
public static readonly string[] WindowsAuthenticationSchemes = new string[] { "Negotiate", "NTLM" };
public static readonly string WindowsAuthenticationDisplayName = "Windows";
public static string InvalidCredentialsErrorMessage = "Invalid username or password";
}

fyi, GetLogoutContextAsync returns also a null-PostLogoutRedirectUri when the configured URI in Config.cs does not match with the configured URI of the JS-Client.
If you are using EF core based configuration you need to verify the URI in the ClientPostLogoutRedirectUris table.
Doesn't seem to apply to your problem but worth mentioning.

I want to share my experience of solving issues with null PostLogoutRedirectUri value.
To initiate Logout process you must first call SignOut("Cookies", "oidc") on mvc client side. Otherwise you will have null logoutId value on Identity Server side. Example endpoint in my HomeController:
public IActionResult Logout()
{
return SignOut("Cookies", "oidc");
}
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
}
}
Hope it helps!

For me what solve the null was the bellow option set up on mvc client
o.SignedOutCallbackPath = new PathString("/signout-callback-oidc");

Try configuring the LogoutUrl for the MVC client!

Related

Blazor WASM: Add JWT auth with custom API

I have built an API that controls some smart home stuff. To prevent the whole internet from doing so, I added authentication using JWT / Bearer. The API contains endpoints for the smart home stuff aswell as some user management:
API endpoints for users
The login will return a JWT token if credentials were valid. It is also built using .NET 6:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(x =>
{
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])),
};
});
Login Controller:
[HttpPost]
public async Task<IActionResult> Login([FromBody] UserLogin login)
{
var user = await _userService.GetUser(login.Username);
if (user is not null && _userService.IsPasswordCorrect(user, login.Password))
{
var tokens = await _userService.GetJwtAndRefreshToken(user);
return Ok(new LoginResponse { JWT = tokens.Jwt, RefreshToken = tokens.Refreshtoken });
}
return Unauthorized("Wrong username or password!");
}
Now I am trying to build a frontend for this app using blazor. When creating the app, i used the option "individual user accounts" for authentication. It is documented here: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/standalone-with-authentication-library?view=aspnetcore-6.0&tabs=visual-studio
This created the following in the blazow WASM app:
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddOidcAuthentication(options =>
{
// Configure your authentication provider options here.
// For more information, see https://aka.ms/blazor-standalone-auth
builder.Configuration.Bind("Local", options.ProviderOptions);
});
await builder.Build().RunAsync();
appsettings.json looks like this:
{
"Local": {
"Authority": "https://localhost:7110/login",
"ClientId": "33333333-3333-3333-33333333333333333"
}
}
I changed the Authority to my login api url, but doesn't seem to be enough.
Clicking on the login button that was added by default fires this request:
Request
Is there a simple way to use the MS Authorization framework with my custom api?
I spent great amount of time on this. These are my notes from it. Note that I am using IdentityServer. Probably a lot of stuff will be different for you. But it should at least guide you what to check.
It works (for me), but best-practise is not garantee.
My API address is on port 5001, Client is on port 5101
For Client project
Change HttpClient address in Client. Change Http MessageHandler. Change address for public client
var clientBaseAddress = new Uri(builder.Configuration["apiurl"] ?? throw new ArgumentNullException("apirul is null (reading from config file)"));
builder.Services.AddHttpClient("BlazorApp6.ServerAPI", client =>client.BaseAddress = clientBaseAddress)
.AddHttpMessageHandler(sp =>
{//this is need when api is separated. https://code-maze.com/using-access-token-with-blazor-webassembly-httpclient/
var handler = sp.GetService<AuthorizationMessageHandler>()!
.ConfigureHandler(
authorizedUrls: new[] { builder.Configuration["HttpMessageHandlerAuthorizedUrls"] },
scopes: new[] { "BlazorApp6.ServerAPI" }
);
return handler;
});
builder.Services.AddHttpClient<PublicClient>(client => client.BaseAddress = clientBaseAddress);
Add HttpMessageHandlerAuthorizedUrls apiurl to appsettings (example for developement):
"apiurl": "https://localhost:5001",
"HttpMessageHandlerAuthorizedUrls": "https://localhost:5001",
Program.cs AddApiAuthorization is different (set opt.ProviderOptions.ConfigurationEndpoint)
builder.Services.AddApiAuthorization(
//this line is only when address of api consumer is different
opt => opt.ProviderOptions.ConfigurationEndpoint = builder.Configuration["ApiAuthorizationConfigurationEndpoint"]
).AddAccountClaimsPrincipalFactory<CustomUserFactory>();
Add ApiAuthorizationConfigurationEndpoint to appsettings
"ApiAuthorizationConfigurationEndpoint": "https://localhost:5001/_configuration/BlazorApp6.Client"
Change launchSetting to different port
"applicationUrl": "https://localhost:5101;http://localhost:5100",
For api project
Add cors to client app
string developmentCorsPolicy = "dev_cors";
services.AddCors(opt =>
{
opt.AddPolicy(name: developmentCorsPolicy, builder =>
{
builder.WithOrigins("https://localhost:5101", "https://localhost:5201")
.WithMethods("GET", "POST", "PUT", "DELETE")
.AllowAnyHeader();
});
});
//...
if (app.Environment.IsDevelopment())
app.UseCors(developmentCorsPolicy);
There is probably some need to add cors for identiy server, but it works without it.
in case it is needed:
services.AddSingleton<ICorsPolicyService>((container) =>
{
var logger = container.GetRequiredService<ILogger<DefaultCorsPolicyService>>();
return new DefaultCorsPolicyService(logger)
{
AllowAll = true
};
});
Change appsettings IdentityServer section to have some info about client.
This info is obtained in OidcController with requests starting _configuration:
"IdentityServer": {
"Clients": {
"BlazorApp6.Client": {
"Profile": "SPA",
"LogoutUri": "https://localhost:5101/authentication/logout-callback",
"RedirectUri": "https://localhost:5101/authentication/login-callback"
},
},
"Key": {
"Type": "Development"
} }
Note that Profile has changed to SPA (instead of IdentityServerSPA, which means hosted)

Acess token refresh with OIDC in .NET Core 3.1, OnValidatePrincipal is never called/thrown

I am making a POC of a small website that uses Keycloak as an OIDC provider, for now I am just using the "standard" scaffolded website that .NET Core generates. The Privacy page has an authorize attribute so that it can only be accessed if the user is authenticated.
My StartUp.cs looks like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(i => new CookieAuthenticationOptions
{
Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = context =>
{
return OnValidatePrincipal(context);
}
}
})
.AddOpenIdConnect(options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = "http://localhost:8080/auth/realms/WatchList/";
options.RequireHttpsMetadata = false;
options.ClientId = "ClientID";
options.ClientSecret = "20ea1950-af47-4251-85e9-7c4f33189c77";
options.ResponseType = OpenIdConnectResponseType.Code;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.SaveTokens = true;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "groups",
ValidateIssuer = true
};
});
services.AddAuthorization();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
private async Task OnValidatePrincipal(CookieValidatePrincipalContext context)
{
const string accessTokenName = "access_token";
const string refreshTokenName = "refresh_token";
const string expirationTokenName = "expires_at";
if (context.Principal.Identity.IsAuthenticated)
{
var exp = context.Properties.GetTokenValue(expirationTokenName);
if (exp != null)
{
var expires = DateTime.Parse(exp, CultureInfo.InvariantCulture).ToUniversalTime();
if (expires < DateTime.UtcNow)
{
// If we don't have the refresh token, then check if this client has set the
// "AllowOfflineAccess" property set in Identity Server and if we have requested
// the "OpenIdConnectScope.OfflineAccess" scope when requesting an access token.
var refreshToken = context.Properties.GetTokenValue(refreshTokenName);
if (refreshToken == null)
{
context.RejectPrincipal();
return;
}
var cancellationToken = context.HttpContext.RequestAborted;
// Obtain the OpenIdConnect options that have been registered with the
// "AddOpenIdConnect" call. Make sure we get the same scheme that has
// been passed to the "AddOpenIdConnect" call.
//
// TODO: Cache the token client options
// The OpenId Connect configuration will not change, unless there has
// been a change to the client's settings. In that case, it is a good
// idea not to refresh and make sure the user does re-authenticate.
var serviceProvider = context.HttpContext.RequestServices;
var openIdConnectOptions = serviceProvider.GetRequiredService<IOptionsSnapshot<OpenIdConnectOptions>>().Get("Cookies");
var configuration = openIdConnectOptions.Configuration ?? await openIdConnectOptions.ConfigurationManager.GetConfigurationAsync(cancellationToken).ConfigureAwait(false);
// Set the proper token client options
var tokenClientOptions = new TokenClientOptions
{
Address = configuration.TokenEndpoint,
ClientId = openIdConnectOptions.ClientId,
ClientSecret = openIdConnectOptions.ClientSecret
};
var httpClientFactory = serviceProvider.GetService<IHttpClientFactory>();
using var httpClient = httpClientFactory.CreateClient();
var tokenClient = new TokenClient(httpClient, tokenClientOptions);
var tokenResponse = await tokenClient.RequestRefreshTokenAsync(refreshToken, cancellationToken: cancellationToken).ConfigureAwait(false);
if (tokenResponse.IsError)
{
context.RejectPrincipal();
return;
}
// Update the tokens
var expirationValue = DateTime.UtcNow.AddSeconds(tokenResponse.ExpiresIn).ToString("o", CultureInfo.InvariantCulture);
context.Properties.StoreTokens(new[]
{
new AuthenticationToken { Name = refreshTokenName, Value = tokenResponse.RefreshToken },
new AuthenticationToken { Name = accessTokenName, Value = tokenResponse.AccessToken },
new AuthenticationToken { Name = expirationTokenName, Value = expirationValue }
});
// Update the cookie with the new tokens
context.ShouldRenew = true;
}
}
}
}
}
This all works as I expect it would: i can access the Home page without having to authenticate but if I want access to the Privacy page I am redirected to the Keycloak login page and after successfully logging in I have access to the Privacy Page as well.
However I want to access the Acces Token and Refresh Token (because I want to use the access token to access an api) as well, this in itself isn't a problem either:
[Authorize]
public IActionResult Privacy()
{
var accessToken = HttpContext.GetTokenAsync("access_token").Result;
Debug.WriteLine(accessToken);
var refreshToken = HttpContext.GetTokenAsync("refresh_token").Result;
Debug.WriteLine(refreshToken);
return View();
}
The problem is that these might be expired and I want to retrieve a new access token, but this should happen automatically.
I've tried the solution that was proposed here, and like you can see in my StartUp.cs, I "catch" the OnValidatePrincipalEvent and execute the code below.
But here is the problem: for some reason this event is never called. I would expect it to be called everytime I access the Privacy page. However this isn't the case, the event seems not be thrown ever.
Things I've tried:
I've followed this comment: https://stackoverflow.com/a/61396951/9784279, that seems to do exactly what I want, except for the fact that the event isn't thrown.
Checked if other events work like expected: I have confirmed that OnTokenValidated and OnTokenResponseReceived are called like expected, however these are OpenIdConnectEvents and not CookieAuthenticationEvents.
Tried playing with the StartUp.cs file, mainly the order/place of app.UseAuthentication() and app.UseAuthorization(). But this didn't change anything.
I thought maybe I could create an extension method GetOrUpdateTokenAsync on HttpContext that retrieves the access token and if it is expired it will retrieve a new one. The problem is that I don't think I have access to StoreTokens in
CookieValidatePrincipalContext
[1]: https://stackoverflow.com/a/61396951/9784279

How to add roles to claims in IdentityServer4?

I am new to IdentityServer and I have been struggling with this issue all day. So much so that I'm almost about to give up on this. I know this question has been asked over and over again and I have tried many different solutions but none seem to work. Hopefully you can help me push me in the right direction with this.
First I installed the IdentityServer4 templates by running dotnet new -i identityserver4.templates and created a new project with the is4aspid template by running dotnet new is4aspid -o IdentityServer.
After that i created a new IdentityServer database and ran the migrations. By that time I had a the default Identity database structure.
In Config.cs I changed MVC client to the following:
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.Implicit,
ClientSecrets = { new Secret("47C2A9E1-6A76-3A19-F3C0-S37763QB36D9".Sha256()) },
RedirectUris = { "https://localhost:44307/signin-oidc" },
FrontChannelLogoutUri = "https://localhost:44307/signout-oidc",
PostLogoutRedirectUris = { "https://localhost:44307/signout-callback-oidc" },
AllowOfflineAccess = true,
AllowedScopes = { "openid", "profile", "api1", JwtClaimTypes.Role }
},
And changed the GetApis method to this:
public static IEnumerable<ApiResource> GetApis()
{
return new ApiResource[]
{
new ApiResource("api1", "My API #1", new List<string>() { "role" })
};
}
There where of course no users in the database yet so i added a registration form and registered two dummy users, one with the username admin#example.com and one with the username subscriber#example.com.
To assign the roles to these user I created the following method in Startup.cs.
private async Task CreateUserRoles(IServiceProvider serviceProvider) {
var RoleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
var UserManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
IdentityResult adminRoleResult;
IdentityResult subscriberRoleResult;
bool adminRoleExists = await RoleManager.RoleExistsAsync("Admin");
bool subscriberRoleExists = await RoleManager.RoleExistsAsync("Subscriber");
if (!adminRoleExists) {
adminRoleResult = await RoleManager.CreateAsync(new IdentityRole("Admin"));
}
if(!subscriberRoleExists) {
subscriberRoleResult = await RoleManager.CreateAsync(new IdentityRole("Subscriber"));
}
ApplicationUser userToMakeAdmin = await UserManager.FindByNameAsync("admin#example.com");
await UserManager.AddToRoleAsync(userToMakeAdmin, "Admin");
ApplicationUser userToMakeSubscriber = await UserManager.FindByNameAsync("subscriber#example.com");
await UserManager.AddToRoleAsync(userToMakeSubscriber, "Subscriber");
}
In the Configure method of the same class I add the the parameter IServiceProvider services and called the above method like so: CreateUserRoles(services).Wait();. By this time my database did have two roles in it.
Next I created a new solution (within the same project) and in the Startup.cs file of that solution I added the following in the ConfigureServices method.
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options => {
options.SaveTokens = true;
options.ClientId = "mvc";
options.ClientSecret = "32D7A7W0-0ALN-2Q44-A1H4-A37990NN83BP";
options.RequireHttpsMetadata = false;
options.Authority = "http://localhost:5000/";
options.ClaimActions.MapJsonKey("role", "role");
});
After that I added app.UseAuthentication(); in the Configure method of the same class.
Then I created a new page with the following if statements.
if(User.Identity.IsAuthenticated) {
<div>Yes, user is authenticated</div>
}
if(User.IsInRole("ADMIN")) {
<div>Yes, user is admin</div>
}
I logged in with admin#example.com but the second if statement returns False. I inspected all the claims by looping over them like so.
#foreach (var claim in User.Claims) {
<dt>#claim.Type</dt>
<dd>#claim.Value</dd>
}
But there was no role claim to be found, only sid, sub, idp, preferred_username and name.
I tried to get the role in there so that the second if statement returns True but after trying and trying I have not yet been able to make it work. Can someone see what I have to do in order to make this work? I am an absolute beginner in IdentityServer4 and trying my best to understand it. Any help will be appreciated. Thanks in advance!
EDIT 1:
Thanks to this question and this question I got the feeling that I'm on the right track. I have made some modifications but I still can not get it to work. I just tried the following.
Created a new ProfileService class in my IdentityServer project with the following content.
public class MyProfileService : IProfileService {
public MyProfileService() { }
public Task GetProfileDataAsync(ProfileDataRequestContext context) {
var roleClaims = context.Subject.FindAll(JwtClaimTypes.Role);
List<string> list = context.RequestedClaimTypes.ToList();
context.IssuedClaims.AddRange(roleClaims);
return Task.CompletedTask;
}
public Task IsActiveAsync(IsActiveContext context) {
return Task.CompletedTask;
}
}
Next I registered this class in the ConfigureServices method by adding the line services.AddTransient<IProfileService, MyProfileService>();. After that I added a new a new line to the GetIdentityResources method, which looks like this now.
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResource("roles", new[] { "role" })
};
}
I also added the roles to my Mvc client like so: AllowedScopes = { "openid", "profile", "api1", "roles" }.
Next I switched over to the other project and added the following lines in the .AddOpenIdConnect oidc.
options.ClaimActions.MapJsonKey("role", "role", "role");
options.TokenValidationParameters.RoleClaimType = "role";
But still, I cannot get it to work like I want it to. Anyone knows what I am missing?
Two things you need to do to make sure you will get users roles in the claims:
1- In IdentityServer4 project: you need to have implementation for IProfileService
http://docs.identityserver.io/en/latest/reference/profileservice.html
don't forget to add the class in startup.cs file like this
services.AddIdentityServer()
// I just removed some other configurations for clarity
**.AddProfileService<IdentityProfileService>();**
2- In Web Client project's startup.cs file: when configuring the openId, you have to mention this :
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "Identity URL ";
options.RequireHttpsMetadata = true;
options.ClientId = "saas_crm_webclient";
options.ClientSecret = "49C1A7E1-0C79-4A89-A3D6-A37998FB86B0";
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = false;
options.Scope.Add("test.api");
options.Scope.Add("identity.api");
options.Scope.Add("offline_access");
**options.ClaimActions.Add(new JsonKeyClaimAction("role", null, "role"));**
**options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};**
});
Slightly different question, absolutely matching answer.
With the Edit 1, IdP configuration looks enough to supply both identity and access tokens with roles when requested. The only thing left is to configure the client to request the access token (.Net client doesn't do that by default), or just request the roles scope within the identity token.
To get the roles with id_token, the client side config must include options.Scope.Add("roles");
To get the roles with bearer token, that token must be requested by specifying options.ResponseType = "id_token token"; in client side config.
I did it like this in .NET 5:
Add JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); before services.AddAuthentication in Startup.cs.
https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/1349
I also added
services.AddScoped<IProfileService, ProfileService>();
and ProfileService.cs that looks like this to map roles to claims:
public sealed class ProfileService : IProfileService
{
private readonly IUserClaimsPrincipalFactory<ApplicationUser> _userClaimsPrincipalFactory;
private readonly UserManager<ApplicationUser> _userMgr;
private readonly RoleManager<IdentityRole> _roleMgr;
public ProfileService(
UserManager<ApplicationUser> userMgr,
RoleManager<IdentityRole> roleMgr,
IUserClaimsPrincipalFactory<ApplicationUser> userClaimsPrincipalFactory)
{
_userMgr = userMgr;
_roleMgr = roleMgr;
_userClaimsPrincipalFactory = userClaimsPrincipalFactory;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
string sub = context.Subject.GetSubjectId();
ApplicationUser user = await _userMgr.FindByIdAsync(sub);
ClaimsPrincipal userClaims = await _userClaimsPrincipalFactory.CreateAsync(user);
List<Claim> claims = userClaims.Claims.ToList();
claims = claims.Where(claim => context.RequestedClaimTypes.Contains(claim.Type)).ToList();
if (_userMgr.SupportsUserRole)
{
IList<string> roles = await _userMgr.GetRolesAsync(user);
foreach (var roleName in roles)
{
claims.Add(new Claim(JwtClaimTypes.Role, roleName));
if (_roleMgr.SupportsRoleClaims)
{
IdentityRole role = await _roleMgr.FindByNameAsync(roleName);
if (role != null)
{
claims.AddRange(await _roleMgr.GetClaimsAsync(role));
}
}
}
}
context.IssuedClaims = claims;
}
public async Task IsActiveAsync(IsActiveContext context)
{
string sub = context.Subject.GetSubjectId();
ApplicationUser user = await _userMgr.FindByIdAsync(sub);
context.IsActive = user != null;
}
}
Source:
https://ffimnsr.medium.com/adding-identity-roles-to-identity-server-4-in-net-core-3-1-d42b64ff6675

Dynamically set OWIN redirect uri

I am using OWIN to connect up to O365 via the Microsoft Graph API in an ASP.NET MVC app. Everything is setup in Startup.Auth.cs including the Redirect Uri value which currently comes from the web.config. Authentication is working correctly.
As I am using wildcards in the App Registration, the redirect uri can be a variety of values and te user is able authenticate to O365 from any number of pages in the app. Once authenticated I'd like them to be brought back to the page they were just on but because the redirect uri is already set, they are brought back to that page.
How can I modify the redirect uri, elsewhere in my code, after the OWIN identity context has been created?
Below is a snippet of the startup code.
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
private static string redirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
....
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = appId,
Authority = "https://login.microsoftonline.com/organizations/v2.0",
PostLogoutRedirectUri = redirectUri,
RedirectUri = redirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async (context) =>
{
Dictionary<string, string> data = new Dictionary<string, string>();
data.Add("client_id", appId);
data.Add("client_secret", appSecret);
data.Add("code", code);
data.Add("grant_type", "authorization_code");
data.Add("redirect_uri", redirectUri);
...
I had a similar situation. I tied into RedirectToIdentityProvider, to modify the RedirectUri before sending the request to the identity provider. Something like the following
Notifications = new OpenIdConnectAuthenticationNotifications()
{
RedirectToIdentityProvider = async (context) =>
{
context.ProtocolMessage.RedirectUri = "Whatever_You_Want_Here";
}
}
I wanted to dynamically determine the redirect_uri of the application and came up with this solution with the help of above answers and comments:
Notifications = new OpenIdConnectAuthenticationNotifications()
{
RedirectToIdentityProvider = (o) =>
{
o.ProtocolMessage.RedirectUri = DetermineRedirectUri(o.Request);
return Task.CompletedTask;
},
AuthorizationCodeReceived = (o) =>
{
o.TokenEndpointRequest.RedirectUri = DetermineRedirectUri(o.Request);
return Task.CompletedTask;
}
}
And the helper method:
private string DetermineRedirectUri(IOwinRequest request)
{
return request.Scheme + System.Uri.SchemeDelimiter + request.Host + request.PathBase;
}
For anyone coming here and wondering where they should put this, it still goes into your statup.auth.cs (or similar) file. Where you need to put it is here:
public void ConfigureAzureAuth(IAppBuilder app)
{
ServicePointManager.Expect100Continue = true;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls
| SecurityProtocolType.Tls11
| SecurityProtocolType.Tls12
| SecurityProtocolType.Ssl3;
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
// Sets the ClientId, authority, RedirectUri as obtained from web.config
Caption = "Your site",
AuthenticationType = "Your site",
ClientId = clientId,
Authority = authority,
//MetadataAddress = "https://your auth url/.well-known/openid-configuration",
RedirectUri = redirectUri,
// PostLogoutRedirectUri is the page that users will be redirected to after sign-out. In this case, it is using the home page
PostLogoutRedirectUri = "/",
Scope = OpenIdConnectScope.OpenIdProfile,
// ResponseType is set to request the code id_token - which contains basic information about the signed-in user
ResponseType = OpenIdConnectResponseType.CodeIdToken,
// OpenIdConnectAuthenticationNotifications configures OWIN to send notification of failed authentications to OnAuthenticationFailed method
Notifications = new OpenIdConnectAuthenticationNotifications
{
//These are like call backs, so they fire when the event happens.
AuthenticationFailed = OnAuthenticationFailed,
//We can tap into this notification to get the IOwinRequest object
RedirectToIdentityProvider = (o) =>
{
//here is where we set the redirect Uri based on the request we received.
o.ProtocolMessage.RedirectUri = DetermineRedirectUri(o.Request);
return Task.CompletedTask;
},
AuthorizationCodeReceived = (o) =>
{
o.TokenEndpointRequest.RedirectUri = DetermineRedirectUri(o.Request);
return Task.CompletedTask;
}
},
});
}
You will also need the additional method that #brz included.
private string DetermineRedirectUri(IOwinRequest request)
{
return request.Scheme + System.Uri.SchemeDelimiter + request.Host + request.PathBase + "/Account/ExternalLoginCallback/";
}

IdentityServer authentication for admin application in the same host

I have an ASP.NET MVC application for hosting an IdentityServer3, but I want to host Angular + WebAPI 2 custom administration app on that same host. That admin app is using oidc-client library for authentication. Below is my Startup class for configuring IdentityServer and calling UseIdentityServerBearerTokenAuthentication method. As you can see I called that method in a async Task because that happened to soon, before IdentityServer started.
Authentication works, my Angular ajax requests are filled with valid access tokens, but I dont get any Claims on WebApi controllers. ClaimsPrincipal have empty Claims list, and IsAuthenticated is false.
Also my client configuration is properly set. Is there something wrong with this setup?
public class Startup
{
public void Configuration(IAppBuilder app)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Trace()
.CreateLogger();
var factory = new IdentityServerServiceFactory();
factory.Register<IdentityDatabaseModel>(new Registration<IdentityDatabaseModel>(typeof(IdentityDatabaseModel)));
factory.Register<UserDataService>(new Registration<UserDataService>(typeof(UserDataService)));
factory.Register<TokenDataService>(new Registration<TokenDataService>(typeof(TokenDataService)));
factory.Register<ClaimsDataService>(new Registration<ClaimsDataService>(typeof(ClaimsDataService)));
factory.Register<ClientDataService>(new Registration<ClientDataService>(typeof(ClientDataService)));
factory.UserService = new Registration<IUserService>(typeof(UserService));
factory.RefreshTokenStore = new Registration<IRefreshTokenStore, RefreshTokenStore>();
factory.ClientStore = new Registration<IClientStore, ClientStore>();
factory.UseInMemoryScopes(WebApplication1.Models.IS.Scopes.Get());
var options = new IdentityServerOptions
{
SigningCertificate = Certificate.Get(),
Factory = factory,
RequireSsl = false,
LoggingOptions = new LoggingOptions
{
//EnableHttpLogging = true,
EnableKatanaLogging = true,
EnableWebApiDiagnostics = true,
WebApiDiagnosticsIsVerbose = true
},
EnableWelcomePage = false
};
app.UseIdentityServer(options);
#region IdentityServer authentication
JwtSecurityTokenHandler.InboundClaimTypeMap.Clear();
Task.Factory.StartNew(() => {
System.Threading.Thread.Sleep(5000);
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "http://localhost:17343",
RequiredScopes = new[] { "openid", "email", "roles", "profile" },
ClientId = "lsidentity",
ClientSecret = "secret"
});
});
#endregion
}
}
The problem was that I needed to configure IssuerName and SigningCertificate in WebApi configuration, so it looks like this:
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "http://localhost:17343",
//Authority = "http://192.168.254.3:303",
RequiredScopes = new[] { "openid",
"email", "profile" },
IssuerName = "http://localhost:17343", //added this
SigningCertificate = Certificate.Get(), // ...and this
// client credentials for the introspection endpoint
ClientId = "lsidentity",
ClientSecret = "secret".Sha256()
});
There was an issue on github, but I did not find it at first.
https://github.com/IdentityServer/IdentityServer3.AccessTokenValidation/issues/38
There is also no need to call this as Task, it works fine now.

Categories