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.
Related
I have three applications that all use different .NET methodologies (I don't know a better word off-hand). I have a .NET Core 3.1 Web App, a .NET Framework 4.8 MVC Web App, and a .NET Framework 4.6 Web Forms Application. All written in C#.
All three require that I use PKCE (Proof Key for Code Exchange). All three applications use Amazon AWS Cognito for Authentication and Authorization. Everything works well except that the two non-.NET Core apps don't use PKCE and I need them to.
Okay, so my main thinking here is that there might possibly be some "easy breezy" way I can add PKCE support to the .NET Framework applications via the Startup.Auth.cs file or some such were we define the OIDC Auth.
What I've done:
I've been able to find some online articles on how to add PKCE to a site, but they're all manual mechanisms of adding the necessary bits to the query string.
How to secure the Cognito login flow with a state nonce and PKCE
Authorize endpoint
ASP.NET Core using Proof Key for Code Exchange (PKCE)
The .NET Core article was particularly useful for my identification of how my .NET Core app works, but it doesn't help with the other two apps. Overall, the process seems to be broken down into the following steps:
Create a Code_Verifier Call it VERIFIER.
SHA256 that Code_Verifier and base 64 encode it. Call it the CHALLENGE.
Add this to the query string we send to the "authorize" endpoint along with a code_challenge_method which must, for Cognito, be set to S256.
Add to the query string that we send to the "token" endpoint, the Code_Verifier with the value VERIFIER.
In essence, we could manually add the following to the query string that calls the "authorize" endpoint: "&code_challenge=CHALLENGE&code_challenge_method=S256".
And to the query string that calls the "token" endpoint: "&code_verifier=VERIFIER". (I'm pretty sure this token endpoint is a POST.)
I think that's it. So, I could add this stuff manually, somehow, to these calls. However, I would prefer to let the .NET Framework magically do the work for me.
Hence, my overall question. Is there a way for me to modify the .NET code I have to have PKCE added by modifying my code without manually adding it.
I've searched the web, the Microsoft site, and I've fiddled around with the actual code, but I'm not finding any mechanisms for adding PKCE in these methods.
My Examples from my code:
.NET Framework Web Forms App:
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(DefaultAuthenticationTypes.ApplicationCookie);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
CookieManager = new SystemWebCookieManager()
});
ConfigureIdentityProviders(app, DefaultAuthenticationTypes.ApplicationCookie);
}
private void ConfigureIdentityProviders(IAppBuilder app, string signInAsType)
{
var saveTokens = true;
var validateIssuer = true;
var saveTokensValue = ConfigurationManager.AppSettings["Cognito.SaveTokens"];
if (!string.IsNullOrEmpty(saveTokensValue))
{
saveTokens = bool.TryParse(saveTokensValue, out var outResult) && outResult;
}
var validateIssuerValue = ConfigurationManager.AppSettings["Cognito.TokenValidationParameters.ValidateIssuer"];
if (!string.IsNullOrEmpty(validateIssuerValue))
{
validateIssuer = bool.TryParse(validateIssuerValue, out var outResult) && outResult;
}
app.UseCustomOidcAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = ConfigurationManager.AppSettings["Cognito.ClientId"],
ResponseType = ConfigurationManager.AppSettings["Cognito.ResponseType"],
Authority = ConfigurationManager.AppSettings["Cognito.Authority"],
MetadataAddress = ConfigurationManager.AppSettings["Cognito.MetadataAddress"],
ClientSecret = ConfigurationManager.AppSettings["Cognito.ClientSecret"],
RedirectUri = ConfigurationManager.AppSettings["Cognito.RedirectUri"],
SaveTokens = saveTokens,
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = ConfigurationManager.AppSettings["Cognito.TokenValidationParameters.NameClaimType"],
RoleClaimType = ConfigurationManager.AppSettings["Cognito.TokenValidationParameters.RoleClaimType"],
ValidateIssuer = validateIssuer
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = async (context) =>
{
var removePortOnRedirectIdentifierValue = ConfigurationManager.AppSettings["Cognito.RemovePortOnRedirectToIdentityProvider"];
var convertResult = bool.TryParse(removePortOnRedirectIdentifierValue, out var removePortOnRedirectIdentifier);
if (removePortOnRedirectIdentifier && convertResult)
{
var builder = new UriBuilder(context.ProtocolMessage.RedirectUri)
{
Scheme = "https",
Port = -1
};
context.ProtocolMessage.RedirectUri = builder.ToString();
}
}
},
Scope = ConfigurationManager.AppSettings["Cognito.Scope"],
SignInAsAuthenticationType = signInAsType
}
);
}
My .NET Framework MVC App:
public void ConfigureAuth(IAppBuilder app)
{
var loginPath = AuthorizationSettings.Instance.LoginPath;
if (string.IsNullOrEmpty(loginPath))
throw new ArgumentNullException("No value specified for EiHubSettings LoginPath");
// Configure the db context, user manager and signin manager to use a single instance per request
app.CreatePerOwinContext(SecurityDbContext.Create);
app.CreatePerOwinContext<SecurityUserManager>(SecurityUserManager.Create);
app.CreatePerOwinContext<SecurityRoleManager>(SecurityRoleManager.Create);
app.CreatePerOwinContext<SecuritySignInManager>(SecuritySignInManager.Create);
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
// Configure the sign in cookie
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString(loginPath),
CookieManager = new SystemWebCookieManager(),
ExpireTimeSpan = TimeSpan.FromSeconds(
Convert.ToInt32(ConfigurationManager.AppSettings["Cognito.CookieLifetimeInSeconds"])),
Provider = new CookieAuthenticationProvider
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<SecurityUserManager, SecurityUser, int>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentityCallback: (manager, user) => user.CreateIdentityAsync(manager),
getUserIdCallback: (user) => user.GetUserId<int>())
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process.
app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
// Enables the application to remember the second login verification factor such as phone or email.
// Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from.
// This is similar to the RememberMe option when you log in.
app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
if(AppSettingsConfigSingleton.UsesCognito)
{
ConfigureIdentityProviders(app, DefaultAuthenticationTypes.ApplicationCookie);
}
}
private void ConfigureIdentityProviders(IAppBuilder app, string signInAsType)
{
var saveTokens = true;
var validateIssuer = true;
var saveTokensValue = ConfigurationManager.AppSettings["Cognito.SaveTokens"];
if (!string.IsNullOrEmpty(saveTokensValue))
{
saveTokens = bool.TryParse(saveTokensValue, out var outResult) && outResult;
}
var validateIssuerValue = ConfigurationManager.AppSettings["Cognito.TokenValidationParameters.ValidateIssuer"];
if (!string.IsNullOrEmpty(validateIssuerValue))
{
validateIssuer = bool.TryParse(validateIssuerValue, out var outResult) && outResult;
}
app.UseCustomOidcAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = ConfigurationManager.AppSettings["Cognito.ClientId"],
ResponseType = ConfigurationManager.AppSettings["Cognito.ResponseType"],
Authority = ConfigurationManager.AppSettings["Cognito.Authority"],
MetadataAddress = ConfigurationManager.AppSettings["Cognito.MetadataAddress"],
ClientSecret = ConfigurationManager.AppSettings["Cognito.ClientSecret"],
RedirectUri = ConfigurationManager.AppSettings["Cognito.RedirectUri"],
SaveTokens = saveTokens,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = context =>
{
var redirectUri = "/account/openidlogincallback";
if (!string.IsNullOrEmpty(context.AuthenticationTicket.Properties.RedirectUri))
{
redirectUri += $"?returnUrl={context.AuthenticationTicket.Properties.RedirectUri}";
}
context.AuthenticationTicket.Properties.RedirectUri = redirectUri;
return Task.FromResult(0);
},
RedirectToIdentityProvider = context =>
{
var removePortOnRedirectIdentifierValue = ConfigurationManager.AppSettings["Cognito.RemovePortOnRedirectToIdentityProvider"];
var convertResult = bool.TryParse(removePortOnRedirectIdentifierValue, out var removePortOnRedirectIdentifier);
if (removePortOnRedirectIdentifier && convertResult)
{
var builder = new UriBuilder(context.ProtocolMessage.RedirectUri)
{
Scheme = "https", Port = -1
};
context.ProtocolMessage.RedirectUri = builder.ToString();
}
return Task.FromResult(0);
}
},
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = ConfigurationManager.AppSettings["Cognito.TokenValidationParameters.NameClaimType"],
RoleClaimType = ConfigurationManager.AppSettings["Cognito.TokenValidationParameters.RoleClaimType"],
ValidateIssuer = validateIssuer
},
Scope = ConfigurationManager.AppSettings["Cognito.Scope"],
SignInAsAuthenticationType = signInAsType
}
);
}
The .NET Core 3.1 app (yes, I know we need to upgrade to .NET 6), has a "built-in" mechanism for adding PKCE (which befuddled me for the longest time until I saw this -- this option is not explicitly set in my code, but as you can see, it's by default turned on):
So, I'm hoping against hope, that there is something similar in the .NET Framework
In the StartUp class:
// Referred to in CustomOidcHandler
public const string CODE_VERIFIER = "code_verifier";
private const string _CODE_CHALLENGE = "code_challenge";
private const string _CODE_CHALLENGE_METHOD = "code_challenge_method";
private const string _CODE_CHALLENGE_METHOD_S256 = "S256";
In the ConfigureIdentityProviders in Startup
RedirectToIdentityProvider = (context) =>
{
var removePortOnRedirectIdentifierValue = ConfigurationManager.AppSettings["Cognito.RemovePortOnRedirectToIdentityProvider"];
var convertResult = bool.TryParse(removePortOnRedirectIdentifierValue, out var removePortOnRedirectIdentifier);
if (removePortOnRedirectIdentifier && convertResult)
{
var builder = new UriBuilder(context.ProtocolMessage.RedirectUri)
{
Scheme = "https",
Port = -1
};
context.ProtocolMessage.RedirectUri = builder.ToString();
}
//
// We need the code_verifier value passed to the CustomOidcHandler
// so that it can be added to the POST to the /token endpoint
//
string codeVerifier = GenerateCodeVerifier();
HttpCookie cookie = new HttpCookie(CODE_VERIFIER);
cookie.Value = codeVerifier;
HttpContext.Current.Response.Cookies.Add(cookie);
//
// Add the code_challenge to the GET request to Cognito
//
// Methodology found here: https://github.com/IdentityServer/IdentityServer4/issues/4874
//
string codeChallenge = GenerateCodeChallenge(codeVerifier);
context.ProtocolMessage.SetParameter(_CODE_CHALLENGE, codeChallenge);
context.ProtocolMessage.SetParameter(_CODE_CHALLENGE_METHOD, _CODE_CHALLENGE_METHOD_S256);
return Task.CompletedTask;
}
In my CustomOidcHandler file, the GetTokens method:
string codeVerifier = Request != null && Request.Cookies != null && Request.Cookies[Startup.CODE_VERIFIER] != null ? Request.Cookies[Startup.CODE_VERIFIER] : string.Empty;
Dictionary<string, string> post = new Dictionary<string, string>
{
{"client_id", options.ClientId},
{"client_secret", options.ClientSecret},
{"grant_type", "authorization_code"},
{"code", authorizationCode},
{"redirect_uri", options.RedirectUri},
{Startup.CODE_VERIFIER, codeVerifier}
};
Hence, the only way is to "manually" add it.
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)
I am using aspnet core 5.0 webapi with CQRS in my project and already have jwt implementation. Not using role management from aspnet core but manually added for aspnet users table role field and it is using everywhere. In internet I can't find any article to implement keycloak for existing authentication and authorization. My point is for now users login with their email+password, idea is not for all but for some users which they already stored in keycloak, or for some users we will store there, give option login to our app using keycloak as well.
Scenario 1:
I have admin#gmail.com in both in my db and in keycloak and both are they in admin role, I need give access for both to login my app, first scenario already working needs implement 2nd scenarion beside first.
Found only this article which implements securing app (as we have already and not trying to replace but extend)
Medium keycloak
My jwt configuration looks like:
public static IServiceCollection AddCustomAuthentication(this IServiceCollection services,
IConfiguration configuration)
{
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(configuration.GetSection("AppSettings:Token").Value));
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt =>
{
opt.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = key,
ValidateAudience = false,
ValidateIssuer = false,
ClockSkew = TimeSpan.Zero
};
opt.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
}
};
});
return services;
}
My jwt service looks like:
public JwtGenerator(IConfiguration config)
{
_key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.GetSection("AppSettings:Token").Value));
}
public string CreateToken(User user)
{
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, user.Id),
new(ClaimTypes.Email, user.Email),
new(ClaimTypes.Name, user.UserName),
new(ClaimTypes.Role, user.Role.ToString("G").ToLower())
};
var creds = new SigningCredentials(_key, SecurityAlgorithms.HmacSha512);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddMinutes(15),
SigningCredentials = creds
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
My login method looks like:
public async Task<GetToken> Handle(LoginCommand request, CancellationToken cancellationToken)
{
var user = await _userManager.FindByEmailAsync(request.Email);
if (user == null)
throw new BadRequestException("User not found");
UserManagement.ForbiddenForLoginUser(user);
var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, false);
if (result.Succeeded)
{
user.IsRoleChanged = false;
RefreshToken refreshToken = new RefreshToken
{
Name = _jwtGenerator.GenerateRefreshToken(),
DeviceName = $"{user.UserName}---{_jwtGenerator.GenerateRefreshToken()}",
User = user,
Expiration = DateTime.UtcNow.AddHours(4)
};
await _context.RefreshTokens.AddAsync(refreshToken, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
return new GetToken(_jwtGenerator.CreateToken(user),refreshToken.Name);
}
throw new BadRequestException("Bad credentials");
}
My authorization handler:
public static IServiceCollection AddCustomMvc(this IServiceCollection services)
{
services.AddMvc(opt =>
{
var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
opt.Filters.Add(new AuthorizeFilter(policy));
// Build the intermediate service provider
opt.Filters.Add<CustomAuthorizationAttribute>();
}).AddFluentValidation(cfg => cfg.RegisterValidatorsFromAssemblyContaining<CreateProjectCommand>());
return services;
}
What is best practise to implement keycloak authentiaction+authorization beside my current approach and give users to login with two scenarios, normal and keycloak login.
P.S. Ui is different and we are using angular this one just webapi for backend.
Since your login method returns a jwt, you could configure multiple bearer tokens by chaining .AddJwtBearer(), one for your normal login and one for keycloak.
Here is a link to a question that might solve your problem: Use multiple jwt bearer authentication.
Keycloak configuration:
Go to Roles -> Realm Roles and create a corresponding role.
Go to Clients -> Your client -> Mappers.
Create a new role mapper and select "User Realm Role" for Mapper Type, "roles" for Token Claim Name and "String" for Claim JSON Type. Without the mapping the role configured before would be nested somewhere else in the jwt.
You can use the debugger at jwt.io to check if your token is correct. The result should look like this:
{
"exp": 1627565901,
"iat": 1627564101,
"jti": "a99ccef1-afa9-4a62-965b-15e8d33de7de",
// [...]
// roles nested in realm_access :(
"realm_access": {
"roles": [
"offline_access",
"uma_authorization",
"Admin"
]
},
// [...]
// your mapped roles in your custom claim
"roles": [
"offline_access",
"uma_authorization",
"Admin"
]
// [...]
}
Hi I have inherit a system like this:
An Api and many Fronts (spas) they share a common menu with links to navigate to each others but they are different react apps, with different urls. And Azure Active directory to authenticate an the Api is protected with Bearer token.
Something like this:
Now I have authorization requirements with a custom permissions that the business people want to assign to every user, for actions that they can do or not and visibility things.
I want to use Identity4Server with the active directory as an open id provider. Then consume a provider api to get custom permission and put those permissions in the claims. Then in the Api impl policies that demand for specify roles and claims to accomplish the permissions specifications.
Something like this:
Identity4Server config:
services.AddAuthentication()
.AddOpenIdConnect("oidc", "OpenID Connect", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.SaveTokens = true;
options.RequireHttpsMetadata = false;
options.Authority = "https://login.microsoftonline.com/tenant/";
options.ClientId = "ClientId";
options.ClientSecret = "ClientSecret";
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
Api:
services
.AddAuthentication(configure =>
{
configure.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
configure.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Audience = "api";
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
});
var clientsPolicy = new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes("Bearer")
.AddRequirements(new ClaimsAuthorizationRequirement("ClientsModule", new[] { "1" }))
.RequireRole("Admin")
.Build();
services.AddAuthorization(options =>
{
options.AddPolicy("Clients", clientsPolicy);
});
For the react apps I'm using this npm "oidc-client": "1.7.0" and a similar approach to https://medium.com/#franciscopa91/how-to-implement-oidc-authentication-with-react-context-api-and-react-router-205e13f2d49
And the Clients config is: (Provider its quite similar the only thing that change is url localhost:3001)
export const IDENTITY_CONFIG = {
authority: "http://localhost:5000",
clientId: "fronts",
redirect_uri: "http://localhost:3000/signin-oidc",
login: "http://localhost:5000/login",
automaticSilentRenew: false,
loadUserInfo: false,
silent_redirect_uri: "http://localhost:3000/silentrenew",
post_logout_redirect_uri: "http://localhost:3000/signout-callback-oidc",
audience: "fronts",
responseType: "id_token token",
grantType: "password",
scope: "openid api",
webAuthResponseType: "id_token token"
};
If the user login into clients (localhost:3000) front and then navigate to providers (localhost:3001) front it shouldn't login again. To accomplish this I configure all the fronts with the same client id, but I don't know if this is the correct way to do it. Now my config class in identity server is:
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientId = "fronts",
ClientSecrets =
{
new Secret("secret".Sha256())
},
ClientName = "All fronts",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
RedirectUris = { "http://localhost:3000/signin-oidc", "http://localhost:3001/signin-oidc" },
PostLogoutRedirectUris = { "http://localhost:3000/signout-callback-oidc", "http://localhost:3001/signout-callback-oidc" },
AllowedCorsOrigins = { "http://localhost:3000", "http://localhost:3001" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api"
}
}
};
}
Do you think this configuration is the correct way to do it or there is a better approach?
You mentioned
many different react apps, with different urls
but in your code snippet I see only the Clients(localhost:3000).
Anyway, the protocol spec tells us to register as many clients as we need. SSO is the main responsibility of identity provider.
You just need to add RequireConsent = false; to your client def in IdSrv to avoid additional unintended user interaction.
Additionally, nowadays the recommended auth flow for spa-s is "code+pkce". You can take a look at this article in order to get detailed info for transition.
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!