Identity Server 4 | MVC | Set User in Context by RequestPasswordTokenAsync - c#

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();
}

Related

How to sign-in .NET 5 MVC webapp with KeyCloak using OpenIdConnect

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.

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

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

How to access custom claim in aspnet core application authorized using Identity Server

I'm following Identity Server quickstart template, and trying to setup the following
Identity server aspnet core app
Mvc client, that authenticates to is4 and also calls webapi client which is a protected api resource.
The ApplicationUser has an extra column which I add into claims from ProfileService like this:
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var sub = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(sub);
if (user == null)
return;
var principal = await _claimsFactory.CreateAsync(user);
if (principal == null)
return;
var claims = principal.Claims.ToList();
claims.Add(new Claim(type: "clientidentifier", user.ClientId ?? string.Empty));
// ... add roles and so on
context.IssuedClaims = claims;
}
And finally here's the configuration in Mvc Client app ConfigureServices method:
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
}).AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.ClientSecret = "mvc-secret";
options.ResponseType = "code";
options.SaveTokens = true;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("offline_access");
options.Scope.Add("api1");
options.GetClaimsFromUserInfoEndpoint = true;
options.ClaimActions.MapUniqueJsonKey("clientidentifier", "clientidentifier");
});
With GetClaimsFromUserInfoEndpoint set to true I can access the custom claim in User.Identity, but this results in 2 calls for ProfileService.
If I remove or set to false then this claim is still part of access_token, but not part of id_token, and then I can't access this specific claim from context User.
Is there a better way I can access this claim from User principal without resulting in 2 calls (as it's now)? or perhaps reading access_token from context and updating user claims once the token is retrieved?
thanks :)
Turns out that Client object in identity server has this property that does the job:
//
// Summary:
// When requesting both an id token and access token, should the user claims always
// be added to the id token instead of requring the client to use the userinfo endpoint.
// Defaults to false.
public bool AlwaysIncludeUserClaimsInIdToken { get; set; }
As explained in the lib metadata setting this to true for a client, then it's not necessary for the client to go and re-get the claims from endpoint
thanks everybody :)
If you want to access custom claims in client side over those added in identity server just follow these steps, it worked for me. I imagine you implement both client and identity server as separated projects in asp.net core and they are ready, you now want to play with claims or maybe want to authorize by role-claim and so on, alright let's go
create a class that inherits from "IClaimsTransformation" like this:
public class MyClaimsTransformation : IClaimsTransformation
{
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var userName = principal.Identity.Name;
var clone = principal.Clone();
var newIdentity = (ClaimsIdentity)clone.Identity;
var user = config.GetTestUsers().Where(p => p.Username == userName).First();
if (user != null)
{
var lstUserClaims = user.Claims.Where(p => p.Type == JwtClaimTypes.Role).ToList();
foreach (var item in lstUserClaims)
if (!newIdentity.Claims.Where(p => p.ValueType == item.ValueType && p.Value == item.Value).Select(p => true).FirstOrDefault())
newIdentity.AddClaim(item);
}
return Task.FromResult(principal);
}
}
But be aware this class will call multiple times over user authentication so i added a simple code to prevent multiple duplicate claim. also you have user name of authenticated user too.
Next create another class like this:
public class ProfileService : IProfileService
{
//private readonly UserManager<ApplicationUser> userManager;
public ProfileService(/*UserManager<ApplicationUser> userManager*/ /*, SignInManager<ApplicationUser> signInManager*/)
{
//this.userManager = userManager;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
context.AddRequestedClaims(context.Subject.Claims);
var collection = context.Subject.Claims.Where(p => p.Type == JwtClaimTypes.Role).ToList();
foreach (var item in collection)
{
var lst = context.IssuedClaims.Where(p => p.Value == item.Value).ToList();
if (lst.Count == 0)
context.IssuedClaims.Add(item);
}
await Task.CompletedTask;
}
public async Task IsActiveAsync(IsActiveContext context)
{
//context.IsActive = true;
await Task.FromResult(0); /*Task.CompletedTask;*/
}
}
This class will call by several context but it's okay cause we added our custom claim(s) at part #1 at this code
foreach (var item in lstUserClaims)
if (!newIdentity.Claims.Where(p => p.ValueType == item.ValueType && p.Value == item.Value).Select(p => true).FirstOrDefault())
newIdentity.AddClaim(item);
This is your basic startup.cs at identity server side:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options => options.EnableEndpointRouting = false);
services.AddTransient<Microsoft.AspNetCore.Authentication.IClaimsTransformation, MyClaimsTransformation>();
services.AddIdentityServer().AddDeveloperSigningCredential()
.AddInMemoryApiResources(config.GetApiResources())
.AddInMemoryIdentityResources(config.GetIdentityResources())
.AddInMemoryClients(config.GetClients())
.AddTestUsers(config.GetTestUsers())
.AddInMemoryApiScopes(config.GetApiScope())
.AddProfileService<ProfileService>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseIdentityServer();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
}
Pay attention to .AddProfileService<ProfileService>(); and services.AddTransient<Microsoft.AspNetCore.Authentication.IClaimsTransformation, MyClaimsTransformation>();
Now at client side go to startup.cs and do as follows:
.AddOpenIdConnect("oidc", options =>
{
//other code
options.GetClaimsFromUserInfoEndpoint = true;
options.ClaimActions.Add(new JsonKeyClaimAction(JwtClaimTypes.Role, null, JwtClaimTypes.Role));
})
for my sample i tried to use "Role" and authorize users by my custom roles.
Next at your controller class do like this:
[Authorize(Roles = "myCustomClaimValue")] or you can create a class for custom authorization filter.
Note that you define test user in config file in your identity server project and the user has a custom claim like this new claim(JwtClaimTypes.Role, "myCustomClaimValue") and this will be back at lstUserClaims variable.
I am assuming you are passing Authorization header with Bearer JWT token while calling the API. You can read access_token from HttpContext in your API Controller.
var accessToken = await this.HttpContext.GetTokenAsync("access_token");
var handler = new JwtSecurityTokenHandler();
if (handler.ReadToken(accessToken) is JwtSecurityToken jt && (jsonToken.Claims.FirstOrDefault(claim => claim.Type == "sub") != null))
{
var subID = jt.Claims.FirstOrDefault(claim => claim.Type == "sub").Value;
}
NOTE : GetClaimsFromUserInfoEndpoint no need to set explicitly.
Here is a bit of extra info on the subject. By default, IdentityServer doesn't include identity claims in the identity token. It is allowed by setting the AlwaysIncludeUserClaimsInIdToken setting on the client configuration to true. But it is not recommended. The initial identity token is returned from the authorization endpoint via front‑channel communication either through a form post or through the URI. If it's returned via the URI and the token becomes too big, you might hit URI length restrictions, which are still dependent on the browser. Most modern browsers don't have issues with long URIs, but older browsers like Internet Explorer might. This may or may not be of concern to you. Looks like my project is similar to yours. Good luck.

Accessing protected API on IdentityServer4 with Bearer Token

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.

How to refresh access token

I have an Asp.net 2.0 core web application which connects to an Identity server 4 application for authentication. There is also an API involved. The API consumes an access token as a bearer token.
My startup:
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = idsEndPoint;
options.RequireHttpsMetadata = false;
options.ClientId = "testclient";
options.ClientSecret = "secret";
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("testapi");
});
Controller:
In my controllers i can see my tokens and they are all populated and i can use the access token in my API calls.
var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
var refreshToken = await HttpContext.GetTokenAsync(IdentityConstants.HttpContextHeaders.RefreshToken);
var idToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.IdToken);
Question:
My problem occurs after one hour where the access token expires. It appears that it is not automatically being refreshed. I am wondering if this is a setting in my authentication that will cause it to refresh it. However I have been unable to find out how I am supposed to force it to refresh the access token after it has expired.
My current solution is to refresh it myself but I would have thought this would be built into the cookie middleware.
for automatic refresh token, add options.Scope.Add("offline_access"); to AddOpenIdConnect() options.
This approach uses OpenIddict, you need to implement the main configuration inside startup.cs. The next Link is an excellent example of this implementation. Hope be useful
https://github.com/openiddict/openiddict-samples/tree/dev/samples/RefreshFlow
if (request.IsPasswordGrantType())
{
if (!Email_Regex_Validation.Check_Valid_Email_Regex(request.Username))
{
return BadRequest(Resources.RegexEmail);
}
SpLoginUser stored = new SpLoginUser(_context);
string result = stored.Usp_Login_User(request.Username, request.Password);
if (!result.Contains("successfully"))
{
return Forbid(OpenIddictServerDefaults.AuthenticationScheme);
}
// Create a new ClaimsIdentity holding the user identity.
var identity = new ClaimsIdentity(
OpenIddictServerDefaults.AuthenticationScheme,
OpenIdConnectConstants.Claims.Name,
OpenIdConnectConstants.Claims.Role);
identity.AddClaim(Resources.issuer, Resources.secret,
OpenIdConnectConstants.Destinations.IdentityToken);
identity.AddClaim(OpenIdConnectConstants.Claims.Name, request.Username,
OpenIdConnectConstants.Destinations.IdentityToken);
var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), new AuthenticationProperties(), OpenIdConnectServerDefaults.AuthenticationScheme);
ticket.SetScopes(OpenIdConnectConstants.Scopes.OfflineAccess);
// Ask OpenIddict to generate a new token and return an OAuth2 token response.
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}

Categories