A bit of background, I have an IdenityServer 4 project that I use to protect access to an mvc project that I have (Using ASP.NET Identity).
Now what I also wanted was an api that is protected via client credentials that returns some information.
What I did was make a new core api project and this was working fine with the client protection, however, I wanted to move the api so it was within IdenityServer.
e.g. localhost:5000/api/info/getinfo
Now I have moved the code over I get a 500 error when I use the attribute [Authorize(AuthenticationSchemes = "Bearer")]
I can use the DiscoveryClient to get a successful token using the credentials but can't with any request unless they are not authorized.
So in ID I set up my start up like this:
services.AddMvc();
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();
// Configure identity server with in-memory stores, keys, clients and scopes
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryPersistedGrants()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryClients(Config.GetClients(Configuration))
.AddInMemoryApiResources(Config.GetApiResources())
.AddAspNetIdentity<ApplicationUser>();
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = Configuration.GetSection("Authority").Value;
options.RequireHttpsMetadata = false;
options.ApiName = "IdentityInfoApi";
});
And then for my api call that is protected I tag it with: [Authorize(AuthenticationSchemes = "Bearer")]
but this returns me a 500 error, now when I use the tag: [Authorize] it works but that's because the user is logged into the mvc app and the response is an html page and not the json object i want.
At the moment I'm using a unit test to hit the api and the code looks like this:
var client = new HttpClient();
var disco = DiscoveryClient.GetAsync("https://localhost:5000").Result;
var tokenClient = new TokenClient(disco.TokenEndpoint, "client", "secret");
var tokenResponse = tokenClient.RequestClientCredentialsAsync("IdentityInfoApi").Result;
client.SetBearerToken(tokenResponse.AccessToken);
var response = client.GetAsync("https://localhost:5000/api/info/getinfo").Result;
if (!response.IsSuccessStatusCode)
{
var userResult = response.Content.ReadAsStringAsync().Result;
var result = JsonConvert.DeserializeObject<PagedUserList>(userResult);
Assert.NotNull(result);
}
Is there something wrong with my setup of ID, the client code or can you not use ID in this way?
Thank's for your help
After a lot of playing around I believe I found the fix.
You must define AddAuthentication() before AddIdentity() or in other words you must configure the api before Identity Server
It's fine to do it any way round if your api is external but not if it is within the Identity Server app it'self.
My new code looks like this:
//Configure api
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "https://localhost:5000";
options.RequireHttpsMetadata = false;
options.ApiName = "IdentityInfoApi";
});
//end
services.AddIdentity<ApplicationUser, IdentityRole>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Add application services.
services.AddTransient<IEmailSender, EmailSender>();
services.Configure<AuthMessageSenderOptions>(Configuration.GetSection("SMTP"));
services.AddMvc();
// Configure identity server with in-memory stores, keys, clients and scopes
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryPersistedGrants()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryClients(Config.GetClients(Configuration))
.AddInMemoryApiResources(Config.GetApiResources())
.AddAspNetIdentity<ApplicationUser>();
Hope this helps anyone else
Related
I'm building an ASP.NET Core 3.1 Web API. For Authentication I'm using right now IdentyServer4. Now I got the additional requirement to apply Mutual TLS. When applying this this results in the following code in my Startup.cs (using: IdentityServer4.AccessTokenValidation (3.0.1) and Microsoft.AspNetCore.Authentication.Certificate (3.1.3)):
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "<baseaddress>";
options.ApiName = "<API>";
});
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
...
}
Now I'm facing the issue that my ClaimsPrincipal is overwritten by the Microsoft.AspNetCore.Authentication.Certificate. This is not desired because we use the claims from IdentityServer4 for allowing/denying functionality.
What's recommended in this situation?
These two lines are the problem in your code
services.AddAuthentication("Bearer")
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
You are overriding the default authentication scheme to certificate.
You should just add your certificate authentication
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "<baseaddress>";
options.ApiName = "<API>";
})
.AddCertificate(options =>
{
...
}
And use this code to authenticate with certificate
httpContext.AuthenticateAsync(CertificateAuthenticationDefaults.AuthenticationScheme);
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();
}
My applications and APIs are protected using IdentityServer 4.
I have a centralized API for user management (registering new users, updating, deleting and resetting passwords). The token generated by this api will be used by identityserver to reset the user's password.
Problem is I always get invalid token error. I know this has nothing to do with url encoding because forgotten password is handled by identity server and the token generated by identity server works fine. The problem is when a token is generated by different api (even on a single machine).
I looked into creating a common data protection provider but I'm unclear how this is done. Basically, how can I have reset password token created by one api accepted by another?
I'm using Asp Identity's usermanager to generate the reset password token:
var token = await _userManager.GeneratePasswordResetTokenAsync(appUser);
This is how my IdentityServer is setup to use Asp Identity:
services
.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.Lockout.AllowedForNewUsers = true;
options.Lockout.DefaultLockoutTimeSpan = new System.TimeSpan(12, 0, 0);
options.Lockout.MaxFailedAccessAttempts = int.Parse(Configuration["MaxFailedAttempts"]);
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
...
var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
options.Authentication.CookieSlidingExpiration = true;
})
.AddAspNetIdentity<ApplicationUser>()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b =>
b.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
options.DefaultSchema = Globals.SCHEMA_IDS;
})
// this adds the operational data from DB (codes, tokens, consents)
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b =>
b.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
options.DefaultSchema = Globals.SCHEMA_IDS;
// this enables automatic token cleanup. this is optional.
options.EnableTokenCleanup = true;
options.TokenCleanupInterval = 30;
})
.AddProfileService<CustomProfileService>()
.AddSigninCredentialFromConfig(Configuration.GetSection("SigninKeyCredentials"), Logger);
and this is how my UserManagement Api is setup to use Asp Identity:
services.AddTransient<IUserStore<ApplicationUser>, UserStore<ApplicationUser, IdentityRole, ApplicationDbContext>>();
services.AddTransient<IRoleStore<IdentityRole>, RoleStore<IdentityRole, ApplicationDbContext>>();
services.AddTransient<IPasswordHasher<ApplicationUser>, PasswordHasher<ApplicationUser>>();
services.AddTransient<ILookupNormalizer, UpperInvariantLookupNormalizer>();
services.AddTransient<IdentityErrorDescriber>();
var identityBuilder = new IdentityBuilder(typeof(ApplicationUser), typeof(IdentityRole), services);
identityBuilder.AddTokenProvider("Default", typeof(DataProtectorTokenProvider<ApplicationUser>));
services.AddTransient<UserManager<ApplicationUser>>();
Had to move on to other issues and just now getting back to this. I ended up solving this by ensuring that all my APIs and IdentityServer instance was configured to use ASP.NET Core Data Protection. I'm using redis as my distributed caching system and so just had to configure each of my api and identityserver and everything is now using the same keys when generating tokens. Below is what I use in each startup.cs:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddSession();
services.Configure<RedisConfiguration>(Configuration.GetSection("redis"));
services.AddDistributedRedisCache(options =>
{
options.Configuration = Configuration.GetValue<string>("redis:host");
});
var redis = ConnectionMultiplexer.Connect(Configuration.GetValue<string>("redis:host"));
services.AddDataProtection()
.PersistKeysToRedis(redis, "DataProtection-Keys")
.SetApplicationName(<application name>);
services.AddTransient<ICacheService, CacheService>();
...
}
and then don't forget to use the session (in APIs):
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
...
app.UseAuthentication();
app.UseSession();
...
}
use the session (in IdentityServer):
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
...
app.UseIdentityServer();
app.UseSession();
...
}
As this is a 2 months old question you already soled it I guess. My suggestion would be: take a look at the life span of your token. And set it in your startup.cs like this:
services.Configure<DataProtectionTokenProviderOptions>(options =>
{
options.TokenLifespan = TimeSpan.FromHours(tokenlifespan);
});
Hope this will help you out!
I encountered the same "invalid token" issue with IdentityServer4 on ASP.NET Core 3.1.
An answer of Prathamesh Shende on learn.microsoft.com solved it for me:
In the ResetPassword action, the code first needed to be decoded before being passed on to the _userManager.ResetPasswordAsync method. Like so:
var decodedCode = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(model.Code));
Have 2 Web API's created using .Net Core 2.0 and hosted internally in IIS under windows authentication (anonymous disabled) on same server. Both API's run on same service account as well with appropriate permissions/rolegroups in Active Directory. However, get 401 unauthorized error when consuming one API from the other. Using HTTPClient to make API calls. Note that, it works when accessing the 2nd API endpoint directly via browser but not from another API.
Decorated with Authorize filter in controller
[Authorize(Policy = "ValidRoleGroup")]
Start up code in ConfigureServices in both api services as below.
services.AddAuthentication(IISDefaults.AuthenticationScheme);
services.AddAuthorization(options =>
{
options.AddPolicy("ValidRoleGroup", policy => policy.RequireRole(Configuration["SecuritySettings:ValidRoleGroup"]));
});
services.AddMvc(configure =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
configure.Filters.Add(new AuthorizeFilter(policy));
});
services.Configure<IISOptions>(options =>
{
options.AutomaticAuthentication = true;
options.ForwardClientCertificate = true;
});
services.AddMvc();
services.AddScoped<HttpClient>(c => new HttpClient(new HttpClientHandler()
{
UseDefaultCredentials = true,
PreAuthenticate = true,
ClientCertificateOptions = ClientCertificateOption.Automatic,
}));
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});
The 401 errors went away after adding registry entries as described in below article (Method 1)
https://support.microsoft.com/en-us/help/896861/you-receive-error-401-1-when-you-browse-a-web-site-that-uses-integrate
Note that the Value data should be your actual domain URL (XXX.com) and not machine name.
I currently have two different projects, one the MainAPI, that will expose the functions for my web client, and another one, the AuthAPI, which handles all the the auth requests, all built using netcore 2.
If I call the AuthAPI directly, it will handle requests as desired.
When I try to have the MainAPI requests be authenticated by the AuthAPI, it fails to do so, although I can see the requests comming in and out the AuthAPI.
Here is my services authentication configuration in MainAPI Startup.cs:
services.AddAuthentication(option =>
option.DefaultAuthenticateScheme = "Bearer")
.AddJwtBearer(options =>
{
options.MetadataAddress = "http://localhost:5019/auth/api/info";
options.Authority = "http://localhost:5019";
options.Audience = AUDIENCE;
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.BackchannelHttpHandler = new BackChannelHttpHandler();
options.IncludeErrorDetails = true;
});
Here is BackChannelHttpHandler class:
public class BackChannelHttpHandler : HttpMessageHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
HttpClient client = new HttpClient(new HttpClientHandler(), disposeHandler: false);
client.DefaultRequestHeaders.Add("Accept", "application/json");
var res = await client.GetAsync(request.RequestUri);
return res;
}
}
And the controller I am calling as the Authorize annotation as follows:
[Authorize(AuthenticationSchemes = "Bearer")]
As for the AuthAPI, I have Cors configured in Startup.cs:
services.AddCors(options =>
{
options.AddPolicy("CorsConfig",
builder => builder
.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
.Build());
});
In AuthAPI controller I have also the [EnableCors("CorsConfig")] Annotation.
I can get the call to the AuthAPI controller, and it returns a HTTP-200, but in my MainAPI something in the authorization process gets called that Unauthorizes it and my call to the MainAPI controller never gets executed.
My questio is, what am I doing wrong in the MainAPI authentication process, that authenticates by itself the token, and invalidates the request.
pay attention for this parameter options.Audience;
it should have available end-points.
or you can share a key between applications for more productivity. Each end-point can read payload from token, but only auth-server can produce tokens.