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.
Related
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";
...
I have configured my OpenIdConnect within KeyCloak and now I am trying to connect to it using .NET 5 MVC application.
How to sign-in correctly?
This is what I have so far
When I try to access protected resource I get redirected to KeyCloak for a correct "relm" to sign in.
The user can sign in and the application flows through the OpenIDConnect to the method OnTokenValidated.
In this event, I can see that the user has successfully logged in and while debugging see the authentication details
string firstName = idToken.Claims.SingleOrDefault(c => c.Type == JwtRegisteredClaimNames.GivenName)?.Value;
where firstName gets populated to the correct user.
Redirect issue
The redirection to my application keeps going into OnTokenValidated as a loop but the application does not register that the user is signed in
My code looks like thus:
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
const string clientId = "demoClient";
const string clientSecret = "4342abf9-CC85-4cf2-ba83-316c56a523b9"; // representative
const string authority = "http://localhost:8080/auth/realms/demo"; // name of authority
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services
.AddAuthentication(options =>
{
options.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = "oidc";
options.DefaultSignInScheme = "Cookies";
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.CallbackPath = "/home/index";
options.Authority = authority;
options.ClientId = clientId;
options.ClientSecret = clientSecret;
options.SaveTokens = true;
options.ResponseType = OpenIdConnectResponseType.IdTokenToken;
options.RequireHttpsMetadata = false; // dev only
options.GetClaimsFromUserInfoEndpoint = true;
//options.Scope.Add("openid"); // TODO: not sure how to configure
options.Scope.Add("profile");
options.Scope.Add("email");
options.SaveTokens = true;
options.Events = new OpenIdConnectEvents
{
OnAuthorizationCodeReceived = context =>
{
// short lived code used to authorise the application on back channel
return Task.CompletedTask;
},
OnRedirectToIdentityProvider = async n =>
{
//save url to state
// n.ProtocolMessage.State = n.HttpContext.Request.Path.Value.ToString();
},
OnTokenValidated = context =>
{
return Task.CompletedTask;
},
OnTicketReceived = context =>
{
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
context.Response.Redirect("/Home/Error?errormessage = " + context.Exception.Message);
// context.HandleResponse(); // Suppress the exception
return Task.CompletedTask;
},
OnRemoteFailure = context =>
{
context.Response.Redirect("/Home/Error");
context.HandleResponse();
return Task.FromResult(0);
},
};
});
To configure an ASP.NET MVC web application client to authenticate with Keycloak use the following configuration:
Add your client in the realm and set the access type as "confidential"
Make sure that the authorization is enabled
If it is a strongly trusted client, you could also enable the Direct Access Grants, but this is optional.
In the field "Valid Redirect URI" set "https://your.app.uri/signin-oidc" and "https://your.app.uri/signout-callback-oidc"
In the field "Web Origins" set "*"
In the field "Backchannel Logout URL" you may set "https://your.app.uri/signin-oidc"
Then configure your client-Id, Authority and Client Secret as you did in the code that you provided in the question.
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();
}
Similar to how an app can offer authentication via either in-built forms or an external Identity Provider, I'd like to have two authentication options for my web site; In this case a custom token (eg API key) passed in the Authorization header that will be used if verified, and if none found or not valid then Open ID Connect. Cookie Auth for either will then keep a session.
I can get either working separately, but how do I combine them to achieve the above?
The following are the separate implementations.
Both have app.UseAuthentication(); in the Configure method in Startup.cs.
The Open ID implementation has only the following in the ConfigureServices method in Startup.cs, plus the [Authorize] attribute on each controller method I want auth on:
services.AddAuthorization(options =>
{
options.AddPolicy(OpenIdConnectDefaults.AuthenticationScheme, policy =>
policy.RequireClaim(ClaimTypes.Authentication, OpenIdConnectDefaults.AuthenticationScheme));
options.AddPolicy(CookieAuthenticationDefaults.AuthenticationScheme, policy =>
policy.RequireClaim(ClaimTypes.Authentication, CookieAuthenticationDefaults.AuthenticationScheme));
});
services.AddAuthentication(o =>
{
o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
o.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(o =>
{
o.LoginPath = "/security/accessdenied";
o.AccessDeniedPath = "/security/accessdenied";
})
.AddOpenIdConnect(o =>
{
o.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
o.ClientId = oidcClientId;
o.Authority = oidcAuthority;
o.Scope.Add("openid");
o.Scope.Add("profile");
o.Scope.Add("email");
o.TokenValidationParameters = new TokenValidationParameters
{
// Set what is populated in User.Identity.Name
NameClaimType = ClaimTypes.Email
};
});
That's the end of the Open ID Connect implementation.
For the custom SAS Token implementation, there's a few pieces...
I have a custom AuthenticationHandler like so:
public class SasTokenAuthHandler : AuthenticationHandler<SasTokenAuthOptions>
{
public SasTokenAuthHandler(IOptionsMonitor<SasTokenAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// get token and parse
// ...
if (tokenExists)
{
// verify token
// ...
if (isVerifiedAndCurrent)
{
var identity = new ClaimsIdentity(new[] {
new Claim(ClaimTypes.Authentication, SasTokenAuthOptions.Scheme)
});
var claimsPrincipal = new ClaimsPrincipal(identity);
result = AuthenticateResult.Success(new AuthenticationTicket(claimsPrincipal, SasTokenAuthOptions.Scheme));
// Create cookie
var authProperties = new AuthenticationProperties()
{
ExpiresUtc = DateTime.UtcNow.AddDays(10)
};
await Context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal, authProperties);
}
else
{
result = AuthenticateResult.Fail("Could not verify signed data");
}
}
return await Task.FromResult(result);
}
}
In the Startup.cs file:
services.AddScheme<SasTokenAuthOptions, SasTokenAuthHandler>(SasTokenAuthOptions.Scheme, options =>
{
var provider = services.BuildServiceProvider();
options.Logger = provider.GetService<Serilog.ILogger>();
options.SasTokenService = provider.GetService<SasTokenService>();
options.CustomApiAuthSettings = provider.GetService<SasTokenAuthSettings>();
});
services.AddAuthorization(options =>
{
options.AddPolicy(SasTokenAuthOptions.Scheme, policy =>
policy.RequireClaim(ClaimTypes.Authentication, SasTokenAuthOptions.Scheme));
options.AddPolicy(CookieAuthenticationDefaults.AuthenticationScheme, policy =>
policy.RequireClaim(ClaimTypes.Authentication, CookieAuthenticationDefaults.AuthenticationScheme));
});
services.AddAuthentication(o =>
{
o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
o.DefaultAuthenticateScheme = SasTokenAuthOptions.Scheme;
})
.AddCookie(o =>
{
o.LoginPath = "/security/accessdenied";
o.AccessDeniedPath = "/security/accessdenied";
})
And on each controller method I require authentication on:
[Authorize(SasTokenAuthOptions.Scheme)]
Perhaps of note: I've noticed if I just have [Authorize] then the SAS Token auth doesn't work but I'm not sure why.
That's the end of the custom SAS Token implementation.
I've tried adding both sets of code in Startup.cs but Open ID Connect is always what is used.
Authorize constructor take policy name but you want custom authentication.
Use AuthenticationSchemes property
Update:
Try this
[Authorize(AuthenticationSchemes= SasTokenAuthOptions.Scheme)]
All the auth handler code has changed from that in the question, but once corrected, then instead of using policies configure the auth services as specified here. Thatis, don't include a default scheme but instead specify all schemes in the Authorize attribute with comma separation. Eg.
[Authorize(AuthenticationSchemes = AuthSchemes)]
public class MixedController : Controller
private const string AuthSchemes = CookieAuthenticationDefaults.AuthenticationScheme + "," + JwtBearerDefaults.AuthenticationScheme + "," + OpenIdConnectDefaults.AuthenticationScheme;
I've noticed that the scheme you want to challenge the user (eg redirect) should be the last one.