I previously have a working project that uses IdentityServer and .Net Core 2.2
I recently did an upgrade on the project's .Net version from Core 2.2 to .Net 5. Also updated every packages into the latest version. Did some tweaks on my code and finally got rid of all the errors. I'm testing my Identity server functionality on my WEB API, and for some reason, the HttpContext.User.Claims return empty.
Here's my code for it.
public Guid? CurrentUserId
{
get
{
var claimNameIdentifier = User.Claims.FirstOrDefault(a => a.Type == ClaimTypes.NameIdentifier)?.Value;
return claimNameIdentifier != null ? Guid.Parse(claimNameIdentifier) : (Guid?)null;
}
}
I have this on a base controller to be implemented by my endpoint controllers to get the user id on the JWT accessing my endpoint. Not sure if this is due to the updates as this was working before.
Here's my Config for my identity server
public static IEnumerable<IdentityResource> IdentityResources =>
new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile()
};
public static IEnumerable<ApiResource> ApiResources =>
new List<ApiResource>
{
new ApiResource
{
Name = "ssi.api",
DisplayName = "Standing Settlement Instructions API"
}
};
public static IEnumerable<ApiScope> ApiScopes =>
new List<ApiScope>
{
new ApiScope
{
Name = "ssi.api",
DisplayName = "Standing Settlement Instructions API"
}
};
public static IEnumerable<Client> Clients =>
new List<Client>
{
new Client
{
ClientId = "ssi.front",
ClientName = "SSI Client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
// secret for authentication
ClientSecrets =
{
new Secret("my secret".Sha256())
},
// scopes that client has access to
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"ssi.api",
JwtClaimTypes.Role
},
AccessTokenLifetime = 14400,
AllowOfflineAccess = true,
}
};
Here's how my ConfigureServices look like:
// Database
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer("Server=localhost; Database = SsiDB; Integrated Security = SSPI; MultipleActiveResultSets=true;"));
#region Identity
services.AddIdentity<User, Role>(options =>
{
options.Password.RequiredLength = 8;
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.User.RequireUniqueEmail = true;
})
.AddRoles<Role>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
var identityBuilder = services.AddIdentityServer()
.AddInMemoryPersistedGrants()
.AddInMemoryClients(IdentityConfig.Clients)
.AddInMemoryApiResources(IdentityConfig.ApiResources)
.AddInMemoryApiScopes(IdentityConfig.ApiScopes)
.AddAspNetIdentity<User>();
identityBuilder.Services.AddTransient<IResourceOwnerPasswordValidator, OwnerPasswordValidator>();
identityBuilder.Services.AddTransient<IProfileService, IdentityProfileService>();
identityBuilder.AddSigningCredential(new X509Certificate2("StandingSettlementInstructionsIdentityAuth.pfx", "", X509KeyStorageFlags.MachineKeySet)); //release
#endregion
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = appSettings.ApiUrl;
options.Audience = "ssi.api";
options.RequireHttpsMetadata = false;
});
And this is my configure
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseHsts();
string appDataPath = Directory.GetCurrentDirectory() + #"\AppData";
if (!Directory.Exists(appDataPath))
Directory.CreateDirectory(appDataPath);
app.UseSwaggerUI(o =>
{
o.DocumentTitle = "Standing Settlement Instructions Api Documentation";
o.RoutePrefix = "api-docs";
o.SwaggerEndpoint("/swagger/v1/swagger.json", "Version 1");
});
app.UseSwagger();
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors("CorsPolicy");
app.UseIdentityServer();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<NotificationHubService>("/NotificationHubService");
});
app.Run((context =>
{
context.Response.Redirect("api-docs");
return Task.CompletedTask;
}));
}
Things I tried:
added this on the ConfigureService services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); and use it on my base controller. same result
Re-arrange the calls on Configure method based on some google searches
Added claims on API Resource and API Scopes. Same result
Some observation. Accessing localhost/.well-known/openid-configuration displays empty supported claims, when I added claims on either API Resource or API Scopes, it does appear in there but still getting the same issue when extracting claims. Still empty.
Any help will be highly appreciated. Been pulling my hair out on this. Thanks!
The first step to debug claims issues is to actually look at what does the access token actually contain? Use a tool like https://jwt.io/ to do that.
Then Microsoft and IdentityServer have different opinion on what the name of the claims should be, so you need to point out, which claim is the name claim, by using:
.AddJwtBearer(opt =>
{
opt.TokenValidationParameters.RoleClaimType = "roles";
opt.TokenValidationParameters.NameClaimType = "name";
...
Related
I have a problem with Identity Server. Recently i got a task on an internship to implement the Identity Server 4 authentification, so first of all I decided to connect IS4 with default MVC client. I figured out how to do it with cookies, as they are used by default (turned out it was easy enough if using templates). But then I got task to use JWT tokens. And that's where I have a problem. All tutorials say that I have to rewrite my IS4 project from the very beginning, but is there an opportunity to use the IS4 template?
Identity server startup file using cookies(by default .Net 3.1 is used)
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
// see https://identityserver4.readthedocs.io/en/latest/topics/resources.html
options.EmitStaticAudienceClaim = true;
})
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiScopes(Config.GetApiScopes())
.AddInMemoryClients(Config.GetClients())
.AddAspNetIdentity<ApplicationUser>();
// not recommended for production - you need to store your key material somewhere secure
builder.AddDeveloperSigningCredential();
services.AddAuthentication()
.AddGoogle(options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
// register your IdentityServer with Google at https://console.developers.google.com
// enable the Google+ API
// set the redirect URI to https://localhost:5001/signin-google
options.ClientId = "copy client ID from Google here";
options.ClientSecret = "copy client secret from Google here";
});
}
public void Configure(IApplicationBuilder app)
{
if (Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
app.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
});
}
IS Config.cs file
public static IEnumerable<ApiResource> GetApiResources()
{
yield return new ApiResource("SwaggerAPI");
yield return new ApiResource("OrdersAPI");
}
public static IEnumerable<IdentityResource> GetIdentityResources()
{
yield return new IdentityResources.OpenId();
yield return new IdentityResources.Profile();
}
/// <summary>
/// IdentityServer4 version 4.x.x changes
/// </summary>
/// <returns></returns>
public static IEnumerable<ApiScope> GetApiScopes()
{
yield return new ApiScope("SwaggerAPI", "Swagger API");
yield return new ApiScope("blazor", "Blazor WebAssembly");
yield return new ApiScope("OrdersAPI", "Orders API");
}
public static IEnumerable<Client> GetClients() =>
new List<Client>
{
new Client
{
ClientId = "add_mvc",
ClientSecrets = { new Secret("add_mvc_secret".ToSha256()) },
AllowedGrantTypes = GrantTypes.Code,
AllowedScopes =
{
"OrdersAPI",
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
},
RedirectUris = {"https://localhost:7272/signin-oidc"},
PostLogoutRedirectUris = {"https://localhost:7272/signout-callback-oidc"},
RequireConsent = false,
AccessTokenLifetime = 5,
AllowOfflineAccess = true
// AlwaysIncludeUserClaimsInIdToken = true
}
};
MVC client program.cs file(.NET 6.0)
builder.Services.AddAuthentication(config =>
{
config.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
config.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, config =>
{
config.Authority = "https://localhost:5001";
config.ClientId = "add_mvc";
config.ClientSecret = "add_mvc_secret";
config.SaveTokens = true;
config.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
config.ResponseType = "code";
config.Scope.Add("OrdersAPI");
config.Scope.Add("offline_access");
config.GetClaimsFromUserInfoEndpoint = true;
config.ClaimActions.MapJsonKey(ClaimTypes.DateOfBirth, ClaimTypes.DateOfBirth);
});
builder.Services.AddAuthorization(config =>
{
config.AddPolicy("HasDateOfBirth", builder =>
{
builder.RequireClaim(ClaimTypes.DateOfBirth);
});
});
builder.Services.AddHttpClient();
// Add services to the container.
builder.Services.AddControllersWithViews();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
I also tried just to add JWT configuration in IS4 startup.cs file, but the error said that jwt tokens can't by used with some methods from signinmanager.
A best practice is to implement and host IdentityServer, Client and the API in separate services. By separating them, it will be much easier to debug and reason about. The client should receive an access token that it then can use to call an API using the HttpClient library.
You do not need to add AddJwtBearer to your client or IdentityServer, instead you add that to your separate API project. The main purpose of .AddJwtBearer is to verify and convert the incoming access token into ClaimsPrincipal user.
You can use this code in the Client to get access to to the various tokens:
string accessToken = await HttpContext.GetTokenAsync("access_token");
string idToken = await HttpContext.GetTokenAsync("id_token");
string refreshToken = await HttpContext.GetTokenAsync("refresh_token");
string tokenType = await HttpContext.GetTokenAsync("token_type");
string accessTokenExpire = await HttpContext.GetTokenAsync("expires_at");
Then you can use the access token to call the API using the HttpClient library.
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
First of the all - I check in google and stack-overflow 2 days....
I found thousands examples and tutorials by still have missing point and don`t have full picture in the head.
So:
My architecture:
1) Identity server
2) 5 +/- MVC websites (Like Production website, Global admin, Help desk, etc...)(which have be protected by identity server )
3) Dozens micro services (which have be protected by identity server )
Now - What I not completely understand:
1) Login:
For now I setup the redirect flow. I Mean.... in website I setup Identity server like:
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "https://localhost:44396";
options.RequireHttpsMetadata = true;
options.ClientId = "<<Here is client ID>>";
options.ClientSecret = "<<HERE IS PASSWORD>>";
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("api1.read");
options.Scope.Add("offline_access");
});
And
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
Now, if user try to open page with Autorize attribute - user redirect to identity server login there and back to protected page. Everything working well.
But....
1) I want login on the MVC page. Without redirect to Identity Server.
I checked internet and found that I need use identityserver resource owner password flow
Then I setup IdentityServer as:
new Client {
ClientId = "myclient",
ClientName = "My first client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,// GrantTypes.HybridAndClientCredentials,
ClientSecrets = new List<Secret> { new Secret("superSecretPassword".Sha256())},
AllowedScopes = new List<string> { "openid", "profile", "api1.read", IdentityServerConstants.StandardScopes.Email},
AllowOfflineAccess = true,
RedirectUris = { "https://localhost:44321/signin-oidc" },
RequireConsent = false
},
And in My MVC I can get token :
public static async Task HandleToken(this HttpClient client, string authority, string clientId, string secret, string apiName)
{
var accessToken = await client.GetRefreshTokenAsync(authority, clientId, secret, apiName);
client.SetBearerToken(accessToken);
}
private static async Task<string> GetRefreshTokenAsync(this HttpClient client, string authority, string clientId, string secret, string apiName)
{
var disco = await client.GetDiscoveryDocumentAsync(authority);
if (disco.IsError) throw new Exception(disco.Error);
var tokenResponse = await client.RequestPasswordTokenAsync(new PasswordTokenRequest
{
UserName = "<<HERE IS USERNAME>>",
Password = "<<HERE IS PASSWORD>>",
Address = disco.TokenEndpoint,
ClientId = clientId,
ClientSecret = secret,
Scope = apiName
});
var user_info = await client.GetUserInfoAsync(new UserInfoRequest() { Address = disco.UserInfoEndpoint, Token = tokenResponse.AccessToken });
Here I have all user claims and Now I want set them in Controller => User
if (!tokenResponse.IsError) return tokenResponse.AccessToken;
return null;
}
Now I get token.....Good.........but
2 Questions:
1) How I can set the User Identity inside Controller.User (ClaimsPrincipal)?
**** UPDATE
I found the one solution
I can use HttpContext.SignInAsync and after I got token and user info from code above - I can do sign in for my Web MVC project and set manually user claims. If this is good approach?
2) All manipulation with User profile data, like ChangePassword, Update FirstName, LastName, etc...
How I need to do this??
Build Microservice for Identity Membership?
P.S - In IdentityServer I use Asp Identity :
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
})
.AddInMemoryIdentityResources(Config.Ids)
.AddInMemoryApiResources(Config.Apis)
.AddInMemoryClients(Config.Clients)
.AddAspNetIdentity<ApplicationUser>();
And last question is:
If I want to use DynamoDB as user store - then I need to build by custom Identity Provider?
(Correct??)
I found this solution in github, and I just need to update to Asp Core 3.1
https://github.com/c0achmcguirk/AspNetIdentity_DynamoDB
For the first question, you just need the configure your API for Identity Server then it will be populated automatically when the client made a proper request. (which includes its access token)
Sample API Configuration
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddCors(r => r.AddDefaultPolicy(o =>
o.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()));
services.AddAuthentication()
.AddJwtBearer(options =>
{
options.Audience = "apix"; // this apis scope
options.Authority = "http://localhost:5000"; // Identity server url
});
services.AddAuthorization(options =>
{
options.DefaultPolicy =
new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build();
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ...
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
// ...
}
And you also need to decorate your API method with [Authorize] attribute.
For the second question, it is a matter of preference. There is a template named QuickStart that includes user operations with IdentityServer4 which handles that in an MVC way. You can also create WEB APIs and expose them. And you may not need to create a separate microservice for that since IdentityServer is a WEB application itself.
For the last question, people usually modify old repos to make it work with DynamoDb. Like this one
Edit:
For the question How to set up the MVC to set User Claims after ResourceOwner flow login
You need to implement a IProfileService service and register it in the startup. (IdentityServer)
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var subject = context.Subject;
var subjectId = subject.Claims.Where(x => x.Type == "sub").FirstOrDefault().Value;
var user = await _userManager.FindByIdAsync(subjectId);
var claims = GetClaimsFromUser(user,context.Caller); // here is the magic method arranges claims according to user and caller
context.IssuedClaims = claims.ToList();
}
I have attempted to search for a solution to this problem, but have not found the right search text.
My question is, how can I configure my IdentityServer so that it will also accept/authorize Api Requests with BearerTokens?
I have an IdentityServer4 configured and running.
I also have configured a Test API on my IdentityServer like below:
[Authorize]
[HttpGet]
public IActionResult Get()
{
return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
}
In my startup.cs ConfigureServices() is as follows:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
...
// configure identity server with stores, keys, clients and scopes
services.AddIdentityServer()
.AddCertificateFromStore(Configuration.GetSection("AuthorizationSettings"), loggerFactory.CreateLogger("Startup.ConfigureServices.AddCertificateFromStore"))
// this adds the config data from DB (clients, resources)
.AddConfigurationStore(options =>
{
options.DefaultSchema = "auth";
options.ConfigureDbContext = builder =>
{
builder.UseSqlServer(databaseSettings.MsSqlConnString,
sql => sql.MigrationsAssembly(migrationsAssembly));
};
})
// this adds the operational data from DB (codes, tokens, consents)
.AddOperationalStore(options =>
{
options.DefaultSchema = "auth";
options.ConfigureDbContext = builder =>
builder.UseSqlServer(databaseSettings.MsSqlConnString,
sql => sql.MigrationsAssembly(migrationsAssembly));
// this enables automatic token cleanup. this is optional.
options.EnableTokenCleanup = true;
options.TokenCleanupInterval = 30;
})
// this uses Asp Net Identity for user stores
.AddAspNetIdentity<ApplicationUser>()
.AddProfileService<AppProfileService>()
;
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = authSettings.AuthorityUrl;
options.RequireHttpsMetadata = authSettings.RequireHttpsMetadata;
options.ApiName = authSettings.ResourceName;
})
and Configure() is as follows:
// NOTE: 'UseAuthentication' is not needed, since 'UseIdentityServer' adds the authentication middleware
// app.UseAuthentication();
app.UseIdentityServer();
I have a client configured to allow Implicit grant types and have included the configured ApiName as one of the AllowedScopes:
new Client
{
ClientId = "47DBAA4D-FADD-4FAD-AC76-B2267ECB7850",
ClientName = "MyTest.Web",
AllowedGrantTypes = GrantTypes.Implicit,
RequireConsent = false,
RedirectUris = { "http://localhost:6200/assets/oidc-login-redirect.html", "http://localhost:6200/assets/silent-redirect.html" },
PostLogoutRedirectUris = { "http://localhost:6200/?postLogout=true" },
AllowedCorsOrigins = { "http://localhost:6200" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"dev.api",
"dev.auth" // <- ApiName for IdentityServer authorization
},
AllowAccessTokensViaBrowser = true,
AllowOfflineAccess = true,
AccessTokenLifetime = 18000,
},
When I use Postman to access the protected API but it always redirects to the Login page even though a valid Bearer Token has been added to the Request header.
Commenting out the [Authorize] attribute will correctly return a response, but of course the User.Claims are empty.
When logging into the IdentityServer (via a browser) and then accessing the API (via the browser) it will also return a response. This time, the User.Claims are available.
There is an example co-hosting a protected API inside IdentityServer: IdentityServerAndApi
I quick comparison between their startup and yours is that they are calling AddJwtBearer instead of AddIdentityServerAuthentication:
services.AddAuthentication()
.AddJwtBearer(jwt => {
jwt.Authority = "http://localhost:5000";
jwt.RequireHttpsMetadata = false;
jwt.Audience = "api1";
});
TheAuthorize attribute also sets the authentication scheme:
[Authorize(AuthenticationSchemes = "Bearer")]
If you want to set a default authentication scheme to be one level above the policies (it is most relevant when you have multiple policies or no policies at all):
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
o.Authority = "http://localhost:5000";
o.RequireHttpsMetadata = false;
o.Audience = "api1";
});
Then you can simple use the [Authorize] tag attribute above the controller's method without polluting each authorization attribute with the sceme:
[Authorize]
public IActionResult GetFoo()
{
}
Found a better solution, configure in the Startup.cs:
services.AddAuthentication()
.AddLocalApi();
services.AddAuthorization(options =>
{
options.AddPolicy(IdentityServerConstants.LocalApi.PolicyName, policy =>
{
policy.AddAuthenticationSchemes(IdentityServerConstants.LocalApi.AuthenticationScheme);
policy.RequireAuthenticatedUser();
});
});
And use in controllers:
[Authorize(IdentityServerConstants.LocalApi.PolicyName)]
public class UserInfoController : Controller
{
...
}
Or even simpler:
services.AddLocalApiAuthentication();
Again, you still need
[Authorize(IdentityServerConstants.LocalApi.PolicyName)]
on your controller/method. And don't forget to add
IdentityServerConstants.LocalApi.ScopeName
to the allowed scopes/requested ones in the token.
See docs for more details.
I had the following code which worked at 1.1.
public static IServiceCollection RegisterRepositoryServices(this IServiceCollection services)
{
services.AddIdentity<ApplicationUser, IdentityRole<int>>(
config => { config.User.RequireUniqueEmail = true;
config.Cookies.ApplicationCookie.LoginPath = "/Account/Login";
config.Cookies.ApplicationCookie.AuthenticationScheme = "Cookie";
config.Cookies.ApplicationCookie.AutomaticAuthenticate = false;
config.Cookies.ApplicationCookie.Events = new CookieAuthenticationEvents()
{
OnRedirectToLogin = async ctx =>
{
if (ctx.Request.Path.StartsWithSegments("/visualjobs") && ctx.Response.StatusCode == 200)
{
ctx.Response.StatusCode = 401;
}
else
{
ctx.Response.Redirect(ctx.RedirectUri);
}
await Task.Yield();
}
};
}).AddEntityFrameworkStores<VisualJobsDbContext, int>()
.AddDefaultTokenProviders();
services.AddEntityFrameworkSqlServer().AddDbContext<VisualJobsDbContext>();
services.AddScoped<IRecruiterRepository, RecruiterRepository>();
services.AddSingleton<IAccountRepository, AccountRepository>();
return services;
}
It now doesn't like the section that makes reference to the config.Cookies....
I've been searching the net, but I cant really find anything to replace this.
You need to call AddIdentity to add the cookie authentication services and then configure the settings using ConfigureApplicationCookie as follows:
services.AddIdentity<ApplicationUser, IdentityRole<int>>(config => {
config.User.RequireUniqueEmail = true;
})
.AddUserStore<UserStore<ApplicationUser, IdentityRole<int>, VisualJobsDbContext, int>>()
.AddDefaultTokenProviders();
services.ConfigureApplicationCookie(o => {
o.LoginPath = new PathString("/Account/Login");
o.Events = new CookieAuthenticationEvents()
{
OnRedirectToLogin = async ctx =>
{
if (ctx.Request.Path.StartsWithSegments("/visualjobs") && ctx.Response.StatusCode == 200)
{
ctx.Response.StatusCode = 401;
}
else
{
ctx.Response.Redirect(ctx.RedirectUri);
}
await Task.Yield();
}
};
});
In addition, in your Configure() method, remember to call the following:
app.UseAuthentication();
Additional reading:
Migrating Authentication and Identity to ASP.NET Core 2.0
Note:
The following section in this article regards the AutomaticAuthenticate option:
Setting Default Authentication Schemes
In 1.x, the AutomaticAuthenticate and AutomaticChallenge
properties of the AuthenticationOptions base class were intended to
be set on a single authentication scheme. There was no good way to
enforce this. In 2.0, these two properties have been removed as
properties on the individual AuthenticationOptions instance. They
can be configured in the AddAuthentication method call within the
ConfigureServices method of Startup.cs:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);