IdentityServer4 Net Core 2 not calling custom iProfileService - profile

I've upgraded my Identity Server project to Net Core 2 and now I am not able to get the iProfileService object to be called to add in custom user claims. It did work in Net Core 1.
Startup.cs ConfigureServices function
// Add application services.
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
services.AddTransient<IProfileService, M25ProfileService>();
//Load certificate
var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath,
"m25id-cert.pfx"), "mypassword");
services.AddIdentityServer()
.AddSigningCredential(cert)
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
//options.EnableTokenCleanup = true;
//options.TokenCleanupInterval = 30;
})
.AddProfileService<M25ProfileService>();
.AddAspNetIdentity<ApplicationUser>();
M25ProfileService.cs
public class M25ProfileService : IProfileService
{
public M25ProfileService(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var user = _userManager.GetUserAsync(context.Subject).Result;
var claims = new List<Claim>
{
new Claim(JwtClaimTypes.GivenName, user.FirstName),
new Claim(JwtClaimTypes.FamilyName, user.LastName),
new Claim(IdentityServerConstants.StandardScopes.Email, user.Email),
new Claim("uid", user.Id),
new Claim(JwtClaimTypes.ZoneInfo, user.TimeZone)
};
if (user.UserType != null)
claims.Add(new Claim("mut", ((int)user.UserType).ToString()));
context.IssuedClaims.AddRange(claims);
return Task.FromResult(0);
}
public Task IsActiveAsync(IsActiveContext context)
{
var user = _userManager.GetUserAsync(context.Subject).Result;
context.IsActive = user != null;
return Task.FromResult(0);
}
}
}
Config.cs
public class Config
{
// try adding claims to id token
public static IEnumerable<IdentityResource> GetIdentityResources()
{
var m25Profile = new IdentityResource(
"m25.profile",
"m25 Profile",
new[]
{
ClaimTypes.Name,
ClaimTypes.Email,
IdentityServerConstants.StandardScopes.OpenId,
JwtClaimTypes.GivenName,
JwtClaimTypes.FamilyName,
IdentityServerConstants.StandardScopes.Email,
"uid",
JwtClaimTypes.ZoneInfo
}
);
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
m25Profile
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
//Try adding claims to access token
return new List<ApiResource>
{
new ApiResource(
"m25api",
"message25 API",
new[]
{
ClaimTypes.Name,
ClaimTypes.Email,
IdentityServerConstants.StandardScopes.OpenId,
JwtClaimTypes.GivenName,
JwtClaimTypes.FamilyName,
IdentityServerConstants.StandardScopes.Email,
"uid",
JwtClaimTypes.ZoneInfo
}
)
};
}
public static IEnumerable<Client> GetClients()
{
// client credentials client
return new List<Client>
{
new Client
{
ClientId = "client",
ClientName = "Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"m25api"
}
},
// Local Development Client
new Client
{
ClientId = "m25AppDev",
ClientName = "me25",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
RequireConsent = false,
RedirectUris = { "http://localhost:4200/authorize.html" },
PostLogoutRedirectUris = { "http://localhost:4200/index.html" },
AllowedCorsOrigins = { "http://localhost:4200" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
JwtClaimTypes.GivenName,
"mut",
"m25api"
},
AllowOfflineAccess = true,
IdentityTokenLifetime = 300,
AccessTokenLifetime = 86400
}
};
}
}
The first thing I'm trying is just to get the identity server to allow me to login and show the user claims similar to the id4 samples. When I login, the standard claims are listed but none of the custom claims. I've put break points in the M25ProfileService class but they never get hit. It seems that ID4 is never using the customer ProfileService class but I do have it in my startup.cs.
I've also tried from my test JS Client and get the same results. Here's a snippet from my JS Client:
var config = {
authority: "http://localhost:5000",
client_id: "m25AppDev",
redirect_uri: "http://localhost:4200/authorize.html",
response_type: "id_token token",
scope:"openid profile m25api",
post_logout_redirect_uri : "http://localhost:4200/index.html"
};
var mgr = new Oidc.UserManager(config);
mgr.getUser().then(function (user) {
if (user) {
log("User logged in", user.profile);
document.getElementById("accessToken").innerHTML = "Bearer " + user.access_token + "\r\n";
}
else {
log("User not logged in");
}
});
function login() {
mgr.signinRedirect();
}
At this point, I'm not sure what to try. I thought if I added the claims to the id token (GetIdentityResources() function from what I understand) and even the access token (GetApiResources() function from what I understand), I'd see the claims but nothing seems to work. Please help! Thanks in advance!
Also, I used to be able to get the custom claims from my client as well as from the Identity Server's own index page that renders after log

Change the order of these lines of code:
.AddProfileService<M25ProfileService>()
.AddAspNetIdentity<ApplicationUser>();
One if overwriting the other.

I figured it out. Thanks to some code on GitHub, I was able to figure out what I was missing. I just needed to add these 2 lines to each client's config in config.cs and all worked perfect!
AlwaysSendClientClaims = true,
AlwaysIncludeUserClaimsInIdToken = true
This works for remote clients. However, I still can't get it to work when I'm on the ID Server itself logging in (not from a client). That's not a big deal for now but could be something in the future. If/When I figure that piece out, I'll try to remember to update my answer. Meanwhile, I hope this helps others.

In addition to the answers above (and beside the fact that the Startup.cs shown in the question already contained the relevant line of code) I'd like to add another, yet very simple cause for why the Profile Service might not be called:
Don't forget to register the service with the dependency injection container!
As having just .AddProfileService<ProfileService>() is not enough.
You would also need:
services.AddScoped<IProfileService, ProfileService>();
Or:
services.AddTransient<IProfileService, ProfileService>();

Related

ASP.NET Web API work cors with identity server 4

I try to connect the ASP.NET Web API (not .NET Core) with identity server.
I use owin as startup but I get a cors error.
Why does cors
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
not work with UseOpenIdConnectAuthentication? It works when I remove it
public class Startup1
{
public void Configuration(IAppBuilder app)
{
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = "insig_spa",
Authority = "https://localhost:5000",
RedirectUri = "https://localhost:5002/auth-callback",
Scope = "openid profile email insigapi.read",
SignInAsAuthenticationType = "cookie",
RequireHttpsMetadata = false,
UseTokenLifetime = false,
RedeemCode = true,
SaveTokens = true,
ClientSecret = "secret",
ResponseType = OpenIdConnectResponseType.Code,
ResponseMode = "query",
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication)
{
// set PKCE parameters
var codeVerifier = CryptoRandom.CreateUniqueId(32);
string codeChallenge;
using (var sha256 = SHA256.Create())
{
var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
codeChallenge = Base64Url.Encode(challengeBytes);
}
n.ProtocolMessage.SetParameter("code_challenge", codeChallenge);
n.ProtocolMessage.SetParameter("code_challenge_method", "S256");
// remember code_verifier (adapted from OWIN nonce cookie)
RememberCodeVerifier(n, codeVerifier);
}
return Task.CompletedTask;
},
AuthorizationCodeReceived = n =>
{
// get code_verifier
var codeVerifier = RetrieveCodeVerifier(n);
// attach code_verifier
n.TokenEndpointRequest.SetParameter("code_verifier", codeVerifier);
return Task.CompletedTask;
}
}
});
}
}

auth0 authorisation in asp.net core app (MVC)

I'am trying to get Auth0 to work in my MVC app.
Although the authentication is working, I cannot seem to get the authorization working.
I've followed this tutorial: https://auth0.com/docs/quickstart/webapp/aspnet-core
my code:
public static IServiceCollection AddAuth0(this IServiceCollection services, IConfiguration configuration)
{
var auth0Options = configuration.GetSection(nameof(Auth0Config))
.Get<Auth0Config>();
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
// options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
// options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}) //do i need this for access_token?
// .AddJwtBearer(options =>
// {
// options.Authority = auth0Options.Domain;
// options.Audience = auth0Options.ApiIdentifier;
//
// options.SaveToken = true;
// options.RequireHttpsMetadata = false;
// })
.AddCookie()
.AddOpenIdConnect(Auth0Constants.Auth0Scheme, options =>
{
options.Authority = $"https://{auth0Options.Domain}";
options.ClientId = auth0Options.ClientId;
options.ClientSecret = auth0Options.ClientSecret;
options.ResponseType = Auth0Constants.ResponseTypeCode;
options.SaveToken = true;
options.Scope.Clear();
options.Scope.Add(Auth0Constants.Auth0Scope.openid.ToString());
options.Scope.Add(Auth0Constants.Auth0Scope.email.ToString());
options.Scope.Add(Auth0Constants.Auth0Scope.profile.ToString());
options.Scope.Add("read:cars");
options.CallbackPath = new PathString("/callback");
options.ClaimsIssuer = Auth0Constants.Auth0Scheme;
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProviderForSignOut = context => OnRedirectToIdentityProviderForSignOut(context, auth0Options),
OnRedirectToIdentityProvider = context => OnRedirectToIdentityProvider(context, auth0Options)
};
});
services.AddAuthorization(options =>
{
options.AddPolicy("read:cars", policy => policy.Requirements.Add(new HasScopeRequirement("read:cars", auth0Options.Domain)));
});
services.AddSingleton<IAuthorizationHandler, HasScopeHandler>();
return services;
}
private static Task OnRedirectToIdentityProvider(RedirectContext context, Auth0Config config)
{
context.ProtocolMessage.SetParameter("audience", config.ApiIdentifier);
return Task.CompletedTask;
}
private static Task OnRedirectToIdentityProviderForSignOut(RedirectContext context, Auth0Config auth0Options)
{
var logoutUri = $"https://{auth0Options.Domain}/v2/logout?client_id={auth0Options.ClientId}";
var postLogoutUri = context.Properties.RedirectUri;
if (!string.IsNullOrEmpty(postLogoutUri))
{
if (postLogoutUri.StartsWith("/"))
{
var request = context.Request;
postLogoutUri = request.Scheme + "://" + request.Host + request.PathBase + postLogoutUri;
}
logoutUri += $"&returnTo={Uri.EscapeDataString(postLogoutUri)}";
}
context.Response.Redirect(logoutUri);
context.HandleResponse();
return Task.CompletedTask;
}
When i look at my charles session, i do see the correct scopes and permissions coming back in the token:
"scope": "openid profile email read:cars",
"permissions": [
"read:cars"
]
But for instance, I cannot get the access_token like they say I can:
var accessToken = await HttpContext.GetTokenAsync("access_token");
this returns null; It is also not in the claims.
On one of my controllers I have: [Authorize("read:cars")] And I get an access denied, do i remove that permission and only use the Authorize, then I am good.
To check if the scope is present:
public class HasScopeRequirement : IAuthorizationRequirement
{
public string Issuer { get; }
public string Scope { get; }
public HasScopeRequirement(string scope, string issuer)
{
Scope = scope;
Issuer = issuer;
}
}
public class HasScopeHandler : AuthorizationHandler<HasScopeRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HasScopeRequirement requirement)
{
// If user does not have the scope claim, get out of here
if (!context.User.HasClaim(c => c.Type == "scope" && c.Issuer == requirement.Issuer))
return Task.CompletedTask;
// Split the scopes string into an array
var scopes = context.User.FindFirst(c => c.Type == "scope" && c.Issuer == requirement.Issuer).Value.Split(' ');
// Succeed if the scope array contains the required scope
if (scopes.Any(s => s == requirement.Scope))
context.Succeed(requirement);
return Task.CompletedTask;
}
}
But I guess this is not my problem, I think it lies within my access_token that i cannot even to read. So I am thinking that I am missing something. Is it due to the DefaultAuthenticationScheme/ChallengeScheme or ...?
thnx to Kirk!
fixed a bit different then my original idea; now i am using roles.
in auth0 I have users, and assign them the desired roles;
and as a rule:
function (user, context, callback) {
context.idToken['http://schemas.microsoft.com/ws/2008/06/identity/claims/roles'] = context.authorization.roles;
callback(null, user, context);
}
I am using http://schemas.microsoft.com/ws/2008/06/identity/claims/roles as a default claim, because .net core maps to that by default.
in my setup I've added:
options.TokenValidationParameters= new TokenValidationParameters
{
RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/roles"
};
Now i can use the Authorize attribute on my controllers
just to be clear; I only have one webapp.If I would go to a different API then I would have to use the access_token off course.

IdentityServer 4 - Adding a custom claim to a User when using Implicit grant type

I'm going through the tutorial of IdentityServer 4 where is explained how to add user authentication with OpenID Connect, it can be found here:
http://docs.identityserver.io/en/latest/quickstarts/3_interactive_login.html
Basically in this tutorial we have an MVC application with a controller action decorated with a Authorized attribute.
Each time a user tries to access that action, in case he/she is not logged in, the MVC application redirects the user to Identity Server so he/she can input the login credentials.
If the credentials are correct, Identity Server redirects back to the MVC application where a page with the User's credentials is shown.
I've concluded the tutorial and now I want to explore a bit more by adding new claims to the token but I haven't been successful so far.
In the tutorial, the scopes OpenId and Profile are added by setting the AllowedScopes on the Client configuration.
I tried to do create a "age" scope and add it in the same manner, but it didn't work.
Does anyone have an idea how I can do this?
The code is shown bellow (commented lines are stuff I already tried).
IdentityServer setup:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// configure identity server with in-memory stores, keys, clients and scopes
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryClients(Config.GetClients())
.AddTestUsers(Config.GetUsers());
}
In memory stores config:
public class Config
{
public static IEnumerable<Client> GetClients()
{
return new List<Client>()
{
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.Implicit,
// where to redirect to after login
RedirectUris = { "http://localhost:5002/signin-oidc" },
// where to redirect to after logout
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
RequireConsent = false,
//AlwaysIncludeUserClaimsInIdToken = true,
//AlwaysSendClientClaims = true,
//AllowAccessTokensViaBrowser = true,
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"age"
}
}
};
}
public static List<TestUser> GetUsers()
{
return new List<TestUser>()
{
new TestUser()
{
SubjectId = "1",
Username = "alice",
Password = "password",
Claims = new List<Claim>()
{
new Claim("age", "15"),
new Claim("name", "Alice"),
new Claim("website", "https://alice.com")
}
},
new TestUser()
{
SubjectId = "2",
Username = "bob",
Password = "password",
Claims = new List<Claim>()
{
new Claim("age", "16"),
new Claim("name", "Bob"),
new Claim("website", "https://bob.com")
}
}
};
}
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResource()
{
DisplayName = "Age",
Name = "age",
UserClaims = new List<string>()
{
"age"
}
}
};
}
}
MVC Application Config:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore()
.AddRazorViewEngine()
.AddAuthorization()
.AddJsonFormatters();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.SaveTokens = true;
});
}
MVC Application Claims Page:
<dl>
#foreach (var claim in User.Claims)
{
<dt>#claim.Type</dt>
<dd>#claim.Value</dd>
}
</dl>
This is the result after a successful login:
sid
ba7ecb47f66524acce04e321b8d2c444
sub
2
idp
local
name
Bob
website
https://bob.com
As you can see the profile claims (name and website) show up, but the custom "age" claim does not.
The answer to the original question is to explicitly add which claims we want to use when setting up OpenId Connect.
We neet to add the following lines inside the .AddOpenIdConnect method:
options.Scope.Clear();
options.Scope.Add("age");
The complete Setup is shown below:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore()
.AddRazorViewEngine()
.AddAuthorization()
.AddJsonFormatters();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.SaveTokens = true;
options.Scope.Clear();
options.Scope.Add("age");
//Add all the claims you need
});
}

How to debug JWT Bearer Error "invalid_token"

I'm trying to secure an existing AspNet Core 2.0 / angular 4 app using jwt. I'm using angular2-jwt for the client part and it works just fine. However when it comes to my WebApi, my token is always rejected(using AuthHttp from angular2-jwt to launch my requests or even with postman). The only response I get is 401 Bearer error="invalid_token". I've checked it with the jwt.io chrome extension and it seems just fine(signature, audience, issuer). I can't find anything in the IIS logs either as to why it is deemed invalid. So my question is how can I get more information on what is wrong with the token ?
Any help will be much appreciated.
For reference here's my startup.cs
public class Startup
{
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.Build();
host.Run();
}
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
IConfigurationSection jwtConf = this.Configuration.GetSection("jwt");
services.Configure<Controls.JWTConf>(Configuration.GetSection("jwt"));
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters =
new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtConf.GetValue<string>("issuer"),
ValidAudience = jwtConf.GetValue<string>("audience"),
IssuerSigningKey = Security.JwtSecurityKey.Create(jwtConf.GetValue<string>("keyBase"))
};
});
services.AddMvc(
config =>
{
var policy = new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireClaim(ClaimTypes.Name)
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
}
).AddJsonOptions(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver());
services.AddNodeServices();
string conn = this.Configuration.GetConnectionString("optimumDB");
services.AddDbContext<TracDbContext>(options =>
options.UseSqlServer(conn));
// Register the Swagger generator, defining one or more Swagger documents
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info { Title = "Angular 4.0 Universal & ASP.NET Core advanced starter-kit web API", Version = "v1" });
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, TracDbContext context)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseStaticFiles();
app.UseAuthentication();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions
{
HotModuleReplacement = true,
HotModuleReplacementEndpoint = "/dist/__webpack_hmr"
});
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
// Enable middleware to serve swagger-ui (HTML, JS, CSS etc.), specifying the Swagger JSON endpoint.
app.MapWhen(x => !x.Request.Path.Value.StartsWith("/swagger", StringComparison.OrdinalIgnoreCase), builder =>
{
builder.UseMvc(routes =>
{
routes.MapSpaFallbackRoute(
name: "spa-fallback",
defaults: new { controller = "Home", action = "Index" });
});
});
}
else
{
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
routes.MapRoute(
"Sitemap",
"sitemap.xml",
new { controller = "Home", action = "SitemapXml" });
routes.MapSpaFallbackRoute(
name: "spa-fallback",
defaults: new { controller = "Home", action = "Index" });
});
app.UseExceptionHandler("/Home/Error");
}
}
}
My token generating controller
[Route("api/token")]
[AllowAnonymous]
public class TokenController : Controller
{
private IOptions<JWTConf> jwt;
public TokenController(IOptions<JWTConf> jwtConf)
{
this.jwt = jwtConf;
}
[HttpPost]
public IActionResult Create([FromBody]string userCode)
{
Model.Entities.Utilisateur user = new Model.Entities.Utilisateur { ID_UTILISATEUR = 6 };
JwtToken token = new JwtTokenBuilder()
.AddSecurityKey(JwtSecurityKey.Create(this.jwt.Value.keyBase))
.AddSubject("User")
.AddIssuer(this.jwt.Value.issuer)
.AddAudience(this.jwt.Value.audience)
.AddClaim(ClaimTypes.Name,user.ID_UTILISATEUR.ToString())
.AddExpiry(1440)
.Build();
var tok = new { token = token.Value };
//return Ok(token);
return Ok(JsonConvert.SerializeObject(tok));
}
}
And finally the controller that rejects the token :
[Produces("application/json")]
public class JobsController : BaseController
{
public JobsController(IConfiguration config, TracDbContext db) : base(config, db)
{
}
// GET: api/Jobs
[HttpGet]
[Route("api/Jobs")]
public IEnumerable<Departement> Get()
{
return new GroupedJobs(Db.GetJobs().ToList());
}
[HttpGet]
[Route("api/Jobs/{id}")]
public JOB_CLIENT Get(int id)
{
return Db.GetDetailsJob(id);
}
}
Found the problem ... turns out I was storing my token with quotes around it. So The authorization header that was being sent looked like this
Bearer "TOKEN"
instead of
Bearer TOKEN
Being new to the whole thing I tought the quotes were being added by the AuthHtpp and were part of the protocol.

IdentityServer4 on Core 2.0 Getting 401 (Unauthorized) from the API

I'm all of a sudden getting 401 errors from my web API project after trying to migrate to the latest version of IdentityServer4 and .NET Core 2. Over the last week, I've made so many changes, I don't know what's right or wrong anymore.
I have a small 3 project solution:
IDSRV: https://localhost:44300
WEB App: https://localhost:44301
API App: https://localhost:44302
Here is my Code to configure IDSV which is stored in my database:
public class Config
{
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource(){
Name = "api.oc.com",
Description = "OC Api",
Scopes = new[] {new Scope("api.oc.com")}
}
};
}
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientId = "oc.com",
ClientName = "OC Website",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
AlwaysIncludeUserClaimsInIdToken = true,
ClientSecrets =
{
new Secret("SomeReallyStrongPassword1!".Sha256())
},
RedirectUris = { "https://localhost:44301/signin-oidc" },
PostLogoutRedirectUris = { "https://localhost:44301/signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api.oc.com",
"offline_access"
},
AllowOfflineAccess = true
}
};
}
}
In my WEB API Startup.cs. I have this in ConfigureServices. I believe my Configure method is ok.
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
o.Authority = "https://localhost:44300";
o.RequireHttpsMetadata = false;
o.Audience = "api.oc.com";
});
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();
This brings me to the WEB Front End trying to authenticate to the WEB API.
I've tried it both with Access Tokens & ID Tokens, but none seem to work.
With Access Token:
var accessToken = await HttpContext.GetTokenAsync("access_token");
var client = new HttpClient();
client.SetBearerToken(accessToken);
var result = await client.GetStringAsync("https://localhost:44302/api/text/welcome");
Or with the client credentials flow:
var disco = await DiscoveryClient.GetAsync("https://localhost:44300");
var tokenClient = new TokenClient(disco.TokenEndpoint, "oc.com", "SomeReallyStrongPassword1!");
var tokenResponse = await tokenClient.RequestClientCredentialsAsync();
var client = new HttpClient();
client.SetBearerToken(tokenResponse.AccessToken);
var result = await client.GetStringAsync("https://localhost:44302/api/text/welcome");
I'm sincerely thankful if anyone has any incite on this. It's been so long looking at security code that my eyeballs are going explode! LOL.
Thanks,
Mike Kushner
After some meddling.. And adding a real certificate, this seems to be working after I created some policies instead of just using the [Authorize] attribute only. Note, that I'm also using IdentityServer4.AccessTokenValidation again.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthorization((options) => {
options.AddPolicy("MustBeValidUser", policybuilder =>
{
policybuilder.RequireAuthenticatedUser();
policybuilder.Requirements = new[] { new MustBeValidUserRequirement() };
});
});
services.AddSingleton<IAuthorizationHandler, MustBeValidUserHandler>();
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "https://localhost:44300";
options.RequireHttpsMetadata = true;
options.ApiName = "api.oc.com";
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();
app.UseDeveloperExceptionPage();
app.UseAuthentication();
app.UseMvc();
}
}

Resources