After a lot of reading, I have found a way to implement a custom JWT bearer token validator as below.
Starup.cs:
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILoggerFactory loggerFactory, IApplicationLifetime appLifetime)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseStaticFiles();
app.UseIdentity();
ConfigureAuth(app);
app.UseMvcWithDefaultRoute();
}
private void ConfigureAuth(IApplicationBuilder app)
{
var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Configuration.GetSection("TokenAuthentication:SecretKey").Value));
var tokenValidationParameters = new TokenValidationParameters
{
// The signing key must match!
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey,
// Validate the JWT Issuer (iss) claim
ValidateIssuer = true,
ValidIssuer = Configuration.GetSection("TokenAuthentication:Issuer").Value,
// Validate the JWT Audience (aud) claim
ValidateAudience = true,
ValidAudience = Configuration.GetSection("TokenAuthentication:Audience").Value,
// Validate the token expiry
ValidateLifetime = true,
// If you want to allow a certain amount of clock drift, set that here:
ClockSkew = TimeSpan.Zero
};
var jwtBearerOptions = new JwtBearerOptions();
jwtBearerOptions.AutomaticAuthenticate = true;
jwtBearerOptions.AutomaticChallenge = true;
jwtBearerOptions.TokenValidationParameters = tokenValidationParameters;
jwtBearerOptions.SecurityTokenValidators.Clear();
//below line adds the custom validator class
jwtBearerOptions.SecurityTokenValidators.Add(new CustomJwtSecurityTokenHandler());
app.UseJwtBearerAuthentication(jwtBearerOptions);
var tokenProviderOptions = new TokenProviderOptions
{
Path = Configuration.GetSection("TokenAuthentication:TokenPath").Value,
Audience = Configuration.GetSection("TokenAuthentication:Audience").Value,
Issuer = Configuration.GetSection("TokenAuthentication:Issuer").Value,
SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256)
};
app.UseMiddleware<TokenProviderMiddleware>(Options.Create(tokenProviderOptions));
}
Custom validator class:
public class CustomJwtSecurityTokenHandler : ISecurityTokenValidator
{
private int _maxTokenSizeInBytes = TokenValidationParameters.DefaultMaximumTokenSizeInBytes;
private JwtSecurityTokenHandler _tokenHandler;
public CustomJwtSecurityTokenHandler()
{
_tokenHandler = new JwtSecurityTokenHandler();
}
public bool CanValidateToken
{
get
{
return true;
}
}
public int MaximumTokenSizeInBytes
{
get
{
return _maxTokenSizeInBytes;
}
set
{
_maxTokenSizeInBytes = value;
}
}
public bool CanReadToken(string securityToken)
{
return _tokenHandler.CanReadToken(securityToken);
}
public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken)
{
//How to access HttpContext/IP address from here?
var principal = _tokenHandler.ValidateToken(securityToken, validationParameters, out validatedToken);
return principal;
}
}
In case of stolen token, I would like to add an additional layer of security to validate that the request is coming from the same client who generated the token.
Questions:
Is there any way I can access HttpContext within the CustomJwtSecurityTokenHandler class so that I could add custom validations based on the current client/requestor?
Is there any other way we can validate the authenticity of the requestor using such method/middleware?
In ASP.NET Core, HttpContext could be obtained using IHttpContextAccessor service. Use DI to pass IHttpContextAccessor instance into your handler and get value of IHttpContextAccessor.HttpContext property.
IHttpContextAccessor service is not registered by default, so you first need to add the following in your Startup.ConfigureServices method:
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
then modify your CustomJwtSecurityTokenHandler class:
private readonly IHttpContextAccessor _httpContextAccessor;
public CustomJwtSecurityTokenHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
_tokenHandler = new JwtSecurityTokenHandler();
}
...
public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken)
{
var httpContext = _httpContextAccessor.HttpContext;
}
You should also use DI technique for JwtSecurityTokenHandler instantiation. Look into Dependency Injection documentation if you are new to all this stuff.
Update: how to manually resolve dependencies (more info here)
modify Configure method to use IServiceProvider serviceProvider:
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILoggerFactory loggerFactory, IApplicationLifetime appLifetime,
IServiceProvider serviceProvider)
{
...
var httpContextAccessor = serviceProvider.GetService<IHttpContextAccessor>();
// and extend ConfigureAuth
ConfigureAuth(app, httpContextAccessor);
...
}
Just to complement another solution and without injection into ISecurityTokenValidator, could be like
In your ISecurityTokenValidator Implementation (CustomJwtSecurityTokenHandler in this case)
public class CustomJwtSecurityTokenHandler : ISecurityTokenValidator {
...
//Set IHttpContextAccessor as public property to set later in Starup class
public IHttpContextAccessor _httpContextAccessor { get; set; };
//Remove injection of httpContextAccessor;
public CustomJwtSecurityTokenHandler()
{
_tokenHandler = new JwtSecurityTokenHandler();
}
...
And in Startup class configure property "CustomJwtSecurityTokenHandler" as global member
public readonly CustomJwtSecurityTokenHandler customJwtSecurityTokenHandler = new()
In ConfigureServices method of Startup class add the global customJwtSecurityTokenHandler.
public void ConfigureServices(IServiceCollection services)
{
...
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(
o =>
{
...
//Add the global ISercurityTokenValidator implementation
o.SecurityTokenValidators.Add(this.customJwtSecurityTokenHandler );
}
);
...
}
Then in Configure method of Startup class pass IHttpContextAccessor instance to property of the global customJwtSecurityTokenHandler (ISecurityTokenValidator)
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILoggerFactory loggerFactory, IApplicationLifetime appLifetime,
IServiceProvider serviceProvider)
{
...
var httpContextAccessor = serviceProvider.GetService<IHttpContextAccessor>();
//And add to property, and not by constructor
customJwtSecurityTokenHandler.httpContextAccessor = httpContextAccessor;
...
}
In my case I've configured SecurityTokenValidator in ConfigureService so In this time there is not exist any instace of IServiceProvider, then in Configure method you can use IServiceProvider to get IHttpContextAccessor
For custom JWT validator, I created a JWTCosumerProvider class inhert to IOAuthBearerAuthenticationProvider. And implement the ValidateIdentity() method to check the identity Claim which i stored the client IP address at first placeļ¼then compare to current request Id address after.
public Task ValidateIdentity(OAuthValidateIdentityContext context)
{
var requestIPAddress = context.Ticket.Identity.FindFirst(ClaimTypes.Dns)?.Value;
if (requestIPAddress == null)
context.SetError("Token Invalid", "The IP Address not right");
string clientAddress = JWTHelper.GetClientIPAddress();
if (!requestIPAddress.Equals(clientAddress))
context.SetError("Token Invalid", "The IP Address not right");
return Task.FromResult<object>(null);
}
JWTHelper.GetClientIPAddress()
internal static string GetClientIPAddress()
{
System.Web.HttpContext context = System.Web.HttpContext.Current;
string ipAddress = context.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];
if (!string.IsNullOrEmpty(ipAddress))
{
string[] addresses = ipAddress.Split(',');
if (addresses.Length != 0)
{
return addresses[0];
}
}
return context.Request.ServerVariables["REMOTE_ADDR"];
}
hope this help!
Related
I'm trying to access HttpContext to get RemoteIpAddress and User-Agent, but within Startup.cs.
public class Startup
{
public Startup(IConfiguration configuration, IHttpContextAccessor httpContextAccessor)
{
Configuration = configuration;
_httpContextAccessor = httpContextAccessor;
}
public IConfiguration Configuration { get; }
public IHttpContextAccessor _httpContextAccessor { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
IdentityModelEventSource.ShowPII = true;
var key = Encoding.ASCII.GetBytes(Configuration.GetValue<string>("claveEncriptacion"));
var ip = _httpContextAccessor.HttpContext.Connection.RemoteIpAddress.ToString();
var userAgent = _httpContextAccessor.HttpContext.Request.Headers["User-Agent"].ToString();
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(x =>
{
//x.Audience = ip + "-" + userAgent;
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = true
};
});
With the previous code I have an error executing the project.
Unable to resolve service for type 'Microsoft.AspNetCore.Http.IHttpContextAccessor' while attempting to activate 'JobSiteMentorCore.Startup'.'
According to the ASP.NET Core documentation , only the following service types can be injected into the Startup constructor when using the Generic Host (IHostBuilder):
IWebHostEnvironment
IHostEnvironment
IConfiguration
So you cannot inject IHttpContextAccessor to Startup constructor.
However you can get DI resolved service in ConfigureServices method of the Startup class as follows:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IYourService, YourService>();
// Build an intermediate service provider
var serviceProvider = services.BuildServiceProvider();
// Resolve the services from the service provider
var yourService = serviceProvider.GetService<IYourService>();
}
But you can not get the HttpContext using IHttpContextAccessor similarly because HttpContext is null unless the code executed during any HttpRequest. So you have to do your desired operation from any custom middleware in Configure method of the Startup class as follows:
public class YourCustomMiddleMiddleware
{
private readonly RequestDelegate _requestDelegate;
public YourCustomMiddleMiddleware(RequestDelegate requestDelegate)
{
_requestDelegate = requestDelegate;
}
public async Task Invoke(HttpContext context)
{
// Your HttpContext related task is in here.
await _requestDelegate(context);
}
}
Then in the Configure method of the Startup class as follows:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMiddleware(typeof(YourCustomMiddleMiddleware));
}
I've finally found a possible solution, using middleware to validate the token.
I created a class ValidationHandler, and this class can use HttpContext.
public class ValidationRequirement : IAuthorizationRequirement
{
public string Issuer { get; }
public string Scope { get; }
public HasScopeRequirement(string scope, string issuer)
{
Scope = scope ?? throw new ArgumentNullException(nameof(scope));
Issuer = issuer ?? throw new ArgumentNullException(nameof(issuer));
}
}
public class ValidationHandler : AuthorizationHandler<ValidationRequirement>
{
IHttpContextAccessor _httpContextAccessor;
public HasScopeHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HasScopeRequirement requirement)
{
var ip = _httpContextAccessor.HttpContext.Connection.RemoteIpAddress.ToString();
var userAgent = _httpContextAccessor.HttpContext.Request.Headers["User-Agent"].ToString();
//context.Succeed(requirement);
return Task.CompletedTask;
}
}
Finally in the Startup.cs class it is necessary to add the following.
services.AddAuthorization(options =>
{
options.AddPolicy("read:messages", policy => policy.Requirements.Add(new HasScopeRequirement("read:messages", "david")));
});
Thank you vidgarga. I would upvote but I have a new account. Your answer helped me on my project. I'm coming from f# land so I am including my implementation for any fsharpers that need this solution.
type HasScopeHandler() =
inherit AuthorizationHandler<HasScopeRequirement>()
override __.HandleRequirementAsync(context, requirement) =
let scopeClaimFromIssuer = Predicate<Claim>(fun (c: Claim) -> c.Type = "scope" && c.Issuer = requirement.Issuer)
let userDoesNotHaveScopeClaim = not (context.User.HasClaim(scopeClaimFromIssuer))
let isRequiredScope s = (s = requirement.Scope)
let claimOrNull = context.User.FindFirst(scopeClaimFromIssuer)
if (userDoesNotHaveScopeClaim) then
Task.CompletedTask
else
match claimOrNull with
| null -> Task.CompletedTask
| claim ->
let scopes = claim.Value.Split(' ')
let hasRequiredScope = scopes.Any(fun s -> isRequiredScope s)
if (hasRequiredScope) then
context.Succeed(requirement)
Task.CompletedTask
else
Task.CompletedTask
I am incorporating AWS-CognitoIdentity Provider in my ASP.Net Core Web Api project and after following the official documentation i still get HttpContext.User NULL. Is there is a step by step guide that someone has used before successfully to get AWS-CognitoIdentity provider working.
I have setup the CognitoIdentity in my Startup.cs and later in my other controllers i am trying to access the User.
public class Startup
{
private static readonly ILog logger = LogManager.GetLogger(typeof(Startup));
private string poolId;
private string appClientId;
private static string providerName;
private static AmazonCognitoIdentityProviderClient provider;
private static CognitoUserPool pool;
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
appClientId = Configuration.GetValue<string>("AWS:UserPoolClientId");
providerName = Configuration.GetValue<string>("AWS:ProviderName");
poolId = Configuration.GetValue<string>("AWS:UserPoolId");
AWSConfigs.RegionEndpoint = RegionEndpoint.EUWest2;
provider = new AmazonCognitoIdentityProviderClient();
pool = new CognitoUserPool(poolId, appClientId, provider, "");
}
public void ConfigureServices(IServiceCollection services)
{
services.Configure<IdentityOptions>(options =>
{
options.Lockout.MaxFailedAccessAttempts = 10;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
});
services.AddCognitoIdentity();
services.AddAuthentication("Bearer").AddJwtBearer(options =>
{
options.Audience = Configuration.GetValue<string>("AWS:UserPoolClientId");
options.Authority = Configuration.GetValue<string>("AWS:ProviderName");
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = Configuration.GetValue<string>("AWS:ProviderName"),
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateLifetime = true,
ValidAudience = Configuration.GetValue<string>("AWS:UserPoolClientId"),
ValidateAudience = true,
IssuerSigningKeyResolver = (s, securityToken, identifier, parameters) =>
{
var json = new WebClient().DownloadString(Configuration.GetValue<string>("AWS:MetadataAddress"));
var keys = JsonConvert.DeserializeObject<JsonWebKeySet>(json).Keys;
return (IEnumerable<SecurityKey>)keys;
},
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
Console.WriteLine("Message Received-------------------------------------------------------------\n");
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
Console.WriteLine("TokenValidated Received-------------------------------------------------------\n");
return Task.CompletedTask;
}
};
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
UserManager<CognitoUser> _userManager,
SignInManager<CognitoUser> _signInManager)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAuthentication();
app.UsePermissions();
app.UseMvc();
app.UseSwagger();
}
}
Controller
namespace DataControllers
{
//[Authorize]
[Route("api/[controller]/[action]")]
public class ContentController : Controller
{
private readonly CognitoUserManager<CognitoUser> _userManager;
public ContentController(UserManager<CognitoUser> userManager)
{
_userManager = userManager as CognitoUserManager<CognitoUser>;
}
[HttpGet]
public async Task<IActionResult> Menu()
{
var email = User.Claims.FirstOrDefault(e => e.Type == "email"); ;
}
}
}
I need to access my DbContext from one handler class which is instantiated in the configure method of Startup.cs class. How Can Instantiate my handler class in order to use the db context registered with the dependency injection container in Startup.ConfigureServices method.
This is my code:
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
var connection = #"Server=MyDb;Initial Catalog=MYDB;Persist Security Info=True; Integrated Security=SSPI;";
services.AddDbContext<iProfiler_ControlsContext>(options => options.UseSqlServer(connection));
//.........
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
//.............
options.SecurityTokenValidators.Add(new MyTokenHandler(MY INSTANCE OF DBCONTEXT HERE));
app.UseJwtBearerAuthentication(options);
//..............
}
Handler Class:
internal class MyTokenHandler : ISecurityTokenValidator
{
private JwtSecurityTokenHandler _tokenHandler;
private iProfiler_ControlsContext _context;
public MyTokenHandler(iProfiler_ControlsContext context)
{
_tokenHandler = new JwtSecurityTokenHandler();
_context = context;
}
public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken)
{
var principal = _tokenHandler.ValidateToken(securityToken, validationParameters, out validatedToken);
var tblVerificationPortalTimeStamps = _context.TblVerificationPortalTimeStamps.ToList();
//......
}
}
First update ConfigureServices to return a service provider from the service collection.
public IServiceProvider ConfigureServices(IServiceCollection services) {
var connection = #"Server=MyDb;Initial Catalog=MYDB;Persist Security Info=True; Integrated Security=SSPI;";
services.AddDbContext<iProfiler_ControlsContext>(options => options.UseSqlServer(connection));
//.........
var provider = services.BuildServiceProvider();
return provider;
}
Next update Configure method to inject IServiceProvider
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILoggerFactory loggerFactory, IServiceProvider provider) {
//.............
var dbContext = provider.GetService<iProfiler_ControlsContext>();
options.SecurityTokenValidators.Add(new MyTokenHandler(dbContext));
app.UseJwtBearerAuthentication(options);
//..............
}
I am working on an Web API application that will be secured used Identity Server 4. The user is authenticated using implicit flow using a JavaScript client.
I am having problems reading the IdentityResource scopes in my Web API client. I think I might be missing something in the configuration when I invoke app.UseIdentityServerAuthentication( ) in my Startup.cs. I am not seeing the JSON data in any of these IdentityResource scopes when I examine the User.Claims collection.
In the JavaScript client, I am able to view the scope data using the oidc-client.js library. But, I cannot read the scope data in my Web API application.
In the javascript client, I can see the scope data stored as json blob by doing this in TypeScript:
class MyService {
private _userManager: Oidc.UserManager;
private _createUserManager(authority: string, origin: string) {
// https://github.com/IdentityModel/oidc-client-js/wiki#configuration
var config : Oidc.UserManagerSettings = {
authority: authority,
client_id: "myapi",
redirect_uri: `${origin}/callback.html`,
response_type: "id_token token",
scope: "openid profile user.profile user.organization ...",
post_logout_redirect_uri: `${origin}/index.html`
};
this._userManager = new Oidc.UserManager(config);
}
// snip
user() {
this._userManager.getUser().then(user => {
const userProfile = JSON.parse(user.profile["user.profile"]);
console.log(userProfile);
const userOrganization = JSON.parse(user.profile["user.organization"]);
console.log(userOrganization);
});
}
}
In my Web API Project, I have:
public class Startup
{
public IConfigurationRoot Configuration { get; set; }
public IContainer Container { get; set; }
public Startup(IHostingEnvironment env)
{
var configurationBuilder = new ConfigurationBuilder();
if (env.IsDevelopment())
{
configurationBuilder
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json");
env.ConfigureNLog("nlog.development.config");
}
else
{
configurationBuilder.AddEnvironmentVariables();
env.ConfigureNLog("nlog.config");
}
Configuration = configurationBuilder.Build();
}
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services
.AddMvcCore()
.AddJsonFormatters()
.AddAuthorization();
services.AddCors();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IRestService, RestService>();
var builder = AutoFacConfig.Create(Configuration);
builder.Populate(services);
Container = builder.Build();
return new AutofacServiceProvider(Container);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseCors(policy =>
{
policy
.WithOrigins(
"http://app.local:5000"
)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
ConfigureExceptionHandling(app, env);
ConfigureOidc(app);
app.UseMvc();
}
private void ConfigureExceptionHandling(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
}
private void ConfigureOidc(IApplicationBuilder app)
{
app.UseCors("default");
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
Authority = Configuration["IdentityServerUrl"],
AllowedScopes = { "api1", "openid", "profile", "user.profile", "user.organization" ... },
ApiName = "myapi",
RequireHttpsMetadata = false
});
}
}
The problem is when I try to access the claims to read its scopes. When the user is authenticated, I pass the token in the header of the request to the Web API endpoint. I have a BaseController in which I would like to read the scopes from, but cannot.
public class BaseController : Controller
{
private ApplicationUser _user;
public string UserId => CurrentUser?.UserInfo.Id;
public string OrganizationId => CurrentUser?.Organization.Id;
[CanBeNull]
public ApplicationUser CurrentUser
{
get
{
var claims = Request.HttpContext.User.Claims.ToList();
_user = new ApplicationUser
{
UserInfo = claims.Where(f => f.Type == "user.profile").Select(s => s.Value).FirstOrDefault(),
OrganizationInfo = claims.Where(f => f.Type == "user.organization").Select(s => s.Value).FirstOrDefault()
};
return _user;
}
}
}
If, I look at the claims collection, I see the following for Types and values:
nbf: 1499441027
exp: 1499444627
iss: https://identity-service....
aud: https://identity-service..../resources
aud: myapi
client_id: myapi
sub: the_user
auth_time: 1499441027
idp: local
scope: openid
scope: profile
scope: user.profile
scope: user.organization
...
scope: myapi
amr: pwd
How do I read scope data from C#?
For user.organization scope, I am expecting to read a data structure like:
{
"Id":"some id",
"BusinessCategory":"some category",
"Name":"some name"
}
Scope is Type and Profile is Value for one of the scopes, that is why it is like that. What value you want to see for profile?? It doesn't have any value as it is a value itself. You are asking for it:
AllowedScopes = { "api1", "openid", "profile", "user.profile", ... },
and that is what you are getting in your scope in the claims list.
If you want to fetch a particular value from the claims list, you can also use JwtRegisteredClaimNames using
User.FindFirst(JwtRegisteredClaimNames.Email).Value
In case this is useful to anyone. I have implemented a hack. I added extra code in my middleware to call the identity server's user info endpoint. The user info endpoint will return the data that I need. I then manually add claims with this data.
I have added this code to my Configure method:
app.Use(async (context, next) =>
{
if (context.User.Identity.IsAuthenticated)
{
using (var scope = Container.BeginLifetimeScope())
{
// fetch resource identity scopes from userinfo endpoint
var restService = scope.Resolve<IRestService>();
var token = context.Request.Headers["Authorization"]
.ToString().Replace("Bearer ", string.Empty);
restService.SetAuthorizationHeader("Bearer", token);
// fetch data from my custom RestService class and
// serialize into my custom ScopeData object
var data = await restService.Get<ScopeData>(
Configuration["IdentityServerUrl"], "connect/userinfo");
if (data.IsSuccessStatusCode)
{
// add identity resource scopes to claims
var claims = new List<Claim>
{
new Claim("user.organization", data.Result.Organization),
new Claim("user.profile", data.Result.UserInfo)
};
}
else
{
sLogger.Warn("Failed to retrieve identity scopes from profile service.");
context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;
}
}
}
await next();
});
I tried looking for questions relating to this but could not find anything.
I have an ASP.NET Core 1.0 app that uses Azure AD B2C for authentication. Signing and registering as well as signing out work just fine. The problem comes when I try to go edit the user's profile. Here is what my Startup.cs looks like:
namespace AspNetCoreBtoC
{
public class Startup
{
private IConfigurationRoot Configuration { get; }
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IConfiguration>(Configuration);
services.AddMvc();
services.AddAuthentication(
opts => opts.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme);
}
// 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();
if (env.IsDevelopment())
{
loggerFactory.AddDebug(LogLevel.Debug);
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AutomaticChallenge = false
});
string signUpPolicyId = Configuration["AzureAd:SignUpPolicyId"];
string signUpCallbackPath = Configuration["AzureAd:SignUpCallbackPath"];
app.UseOpenIdConnectAuthentication(CreateOidConnectOptionsForPolicy(signUpPolicyId, false, signUpCallbackPath));
string userProfilePolicyId = Configuration["AzureAd:UserProfilePolicyId"];
string profileCallbackPath = Configuration["AzureAd:ProfileCallbackPath"];
app.UseOpenIdConnectAuthentication(CreateOidConnectOptionsForPolicy(userProfilePolicyId, false, profileCallbackPath));
string signInPolicyId = Configuration["AzureAd:SignInPolicyId"];
string signInCallbackPath = Configuration["AzureAd:SignInCallbackPath"];
app.UseOpenIdConnectAuthentication(CreateOidConnectOptionsForPolicy(signInPolicyId, true, signInCallbackPath));
app.UseMvc(routes =>
{
routes.MapRoute(
name: "Default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
private OpenIdConnectOptions CreateOidConnectOptionsForPolicy(string policyId, bool autoChallenge, string callbackPath)
{
string aadInstance = Configuration["AzureAd:AadInstance"];
string tenant = Configuration["AzureAd:Tenant"];
string clientId = Configuration["AzureAd:ClientId"];
string redirectUri = Configuration["AzureAd:RedirectUri"];
var opts = new OpenIdConnectOptions
{
AuthenticationScheme = policyId,
MetadataAddress = string.Format(aadInstance, tenant, policyId),
ClientId = clientId,
PostLogoutRedirectUri = redirectUri,
ResponseType = "id_token",
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name"
},
CallbackPath = callbackPath,
AutomaticChallenge = autoChallenge
};
opts.Scope.Add("openid");
return opts;
}
}
}
Here is my AccountController, from where I issue the challenges to the middleware:
namespace AspNetCoreBtoC.Controllers
{
public class AccountController : Controller
{
private readonly IConfiguration config;
public AccountController(IConfiguration config)
{
this.config = config;
}
public IActionResult SignIn()
{
return Challenge(new AuthenticationProperties
{
RedirectUri = "/"
},
config["AzureAd:SignInPolicyId"]);
}
public IActionResult SignUp()
{
return Challenge(new AuthenticationProperties
{
RedirectUri = "/"
},
config["AzureAd:SignUpPolicyId"]);
}
public IActionResult EditProfile()
{
return Challenge(new AuthenticationProperties
{
RedirectUri = "/"
},
config["AzureAd:UserProfilePolicyId"]);
}
public IActionResult SignOut()
{
string returnUrl = Url.Action(
action: nameof(SignedOut),
controller: "Account",
values: null,
protocol: Request.Scheme);
return SignOut(new AuthenticationProperties
{
RedirectUri = returnUrl
},
config["AzureAd:UserProfilePolicyId"],
config["AzureAd:SignUpPolicyId"],
config["AzureAd:SignInPolicyId"],
CookieAuthenticationDefaults.AuthenticationScheme);
}
public IActionResult SignedOut()
{
return View();
}
}
}
I've tried to adapt it from the OWIN example. The problem that I have is that in order to go to edit the profile, I must issue a challenge to the OpenIdConnect middleware that is responsible for that. The problem is that it calls up to the default sign in middleware (Cookies), which realizes the user is authenticated, thus the action must have been something unauthorized, and tries to redirect to /Account/AccessDenied (even though I don't even have anything on that route), instead of going to Azure AD to edit the profile as it should.
Has anyone successfully implemented user profile edit in ASP.NET Core?
Well, I finally solved it. I wrote a blog article on the setup which includes the solution: https://joonasw.net/view/azure-ad-b2c-with-aspnet-core. The issue was ChallengeBehavior, which must be set to Unauthorized, instead of the default value of Automatic. It wasn't possible to define it with the framework ChallengeResult at the moment, so I made my own:
public class MyChallengeResult : IActionResult
{
private readonly AuthenticationProperties authenticationProperties;
private readonly string[] authenticationSchemes;
private readonly ChallengeBehavior challengeBehavior;
public MyChallengeResult(
AuthenticationProperties authenticationProperties,
ChallengeBehavior challengeBehavior,
string[] authenticationSchemes)
{
this.authenticationProperties = authenticationProperties;
this.challengeBehavior = challengeBehavior;
this.authenticationSchemes = authenticationSchemes;
}
public async Task ExecuteResultAsync(ActionContext context)
{
AuthenticationManager authenticationManager =
context.HttpContext.Authentication;
foreach (string scheme in authenticationSchemes)
{
await authenticationManager.ChallengeAsync(
scheme,
authenticationProperties,
challengeBehavior);
}
}
}
Sorry for the name... But this one can be returned from a controller action, and by specifying ChallengeBehavior.Unauthorized, I got everything working as it should.