In an ASP.NET Core 3 application, I need to process information from id_token along with access_token.
The id_token has membership information that is sometimes required to build a policy. Since the membership information can be large, making it part of the access_token is not possible (token exceeds maximum allowed size).
The clients send id_token in x-id-token header and I am looking for a way to extract it and use the claims within.
Right now I have JwtBearer auth configured which works seamlessly with Authorization: Bearer access_token header.
public void ConfigureServices(IServiceCollection services) {
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Authority = $"https://{Configuration["auth:Domain"]}/";
options.Audience = Configuration["auth:Audience"];
});
...
}
AS stated in the question, I needed a step in authorization flow to validate id_token and a membership_id supplied in custom headers. I ended up creating a custom auth requirement handler in the following form
internal class MembershipRequirement : AuthorizationHandler<MembershipRequirement>, IAuthorizationRequirement
{
public MembershipRequirement(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MembershipRequirement requirement)
{
var authFilterCtx = (Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext)context.Resource;
string idToken = authFilterCtx.HttpContext.Request.Headers["x-id-token"];
string membershipId = authFilterCtx.HttpContext.Request.Headers["x-selected-membership-id"];
if (idToken != null && membershipId != null)
{
var identity = ValidateIdToken(idToken).Result;
if (identity != null)
{
var subscriptions = identity.Claims.ToList().FindAll(s => s.Type == "https://example.com/subs").ToList();
var assignments = subscriptions.Select(s => JsonSerializer.Deserialize<Subscription>(s.Value)).ToList();
var membership = assignments.Find(a => a.id == membershipId);
if (membership != null)
{
// assign the id token claims to user identity
context.User.AddIdentity(new ClaimsIdentity(identity.Claims));
context.Succeed(requirement);
}
else { context.Fail(); }
}
else
{
context.Fail();
}
}
return Task.FromResult<object>(null);
}
private async Task<ClaimsPrincipal> ValidateIdToken(string token)
{
try
{
IConfigurationManager<OpenIdConnectConfiguration> configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>($"https://{Configuration["Auth:Domain"]}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
OpenIdConnectConfiguration openIdConfig = await configurationManager.GetConfigurationAsync(CancellationToken.None);
TokenValidationParameters validationParameters =
new TokenValidationParameters
{
IssuerSigningKeys = openIdConfig.SigningKeys,
ValidateIssuer = false,
ValidateAudience = false
};
var validator = new JwtSecurityTokenHandler();
SecurityToken validatedToken;
var identity = validator.ValidateToken(token, validationParameters, out validatedToken);
return identity;
}
catch (Exception e)
{
Console.Writeline($"Error occurred while validating token: {e.Message}");
return null;
}
}
}
internal class Subscription
{
public string name { get; set; }
public string id { get; set; }
}
Then in the public void ConfigureServices(IServiceCollection services) method added a policy to check for membership in the id_token
services.AddAuthorization(options =>
{
options.AddPolicy("RequiredCompanyMembership", policy => policy.Requirements.Add(new MembershipRequirement(Configuration)));
});
For us, this policy is globally applied for all Authorized endpoints.
Related
I am trying to use a bearer token at present and no matter what I do, I get a HTTP 401 unauthorized error.
I am following a guide on JWT implementation I have a extension method that handles the JWT.
public static class AuthenticationExtension
{
public static IServiceCollection AddTokenAuthentication(this IServiceCollection services, IConfiguration config)
{
var secret = config.GetSection("JwtConfig").GetSection("secret").Value;
var key = Encoding.ASCII.GetBytes(secret);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.TokenValidationParameters = new TokenValidationParameters
{
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
// ValidIssuer = "localhost",
//ValidAudience = "localhost"
};
});
return services;
}
}
Also the following is the way the token controller generates the token obv I should be getting values from the header instead email and a password for example.
As that code be replaced if found in code could it not.
var token = jwt.GenerateSecurityToken("fake#email.com");
In my StartUp.cs I simply have the following to add the middle ware to my config. In the services section I conduct a test
services.AddTokenAuthentication(Configuration);
But as you see I get HTTP 401 unauthorised returned.
This is the code from the api/token controller.
[Route("api/[controller]")]
[ApiController]
public class TokenController : ControllerBase
{
private IConfiguration _config;
public TokenController(IConfiguration config)
{
_config = config;
}
[HttpGet]
public string GetRandomToken()
{
var jwt = new JwtService(_config);
var token = jwt.GenerateSecurityToken("fake#email.com");
return token;
}
}
Token from api/token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImZha2VAZW1haWwuY29tIiwibmJmIjoxNTg2NjMzMTkwLCJleHAiOjE1ODY3MTk1OTAsImlhdCI6MTU4NjYzMzE5MH0.SPSErcPpD4f48sWFYQFVUBmTaVtCW8oDw4Np6Tncozo
This is my appSettings.json config I will of course be changing my secret once I have this setup.
Is this enough to secure a api or should you also use client id and secret in terms of api layer. HMAC style.
"JwtConfig": {
"secret": "PDv7DrqznYL6nv7DrqzjnQYO9JxIsWdcjnQYL6nu0f",
"expirationInMinutes": 1440
},
In my case I also failed like you did at first, eventually I followed this tutorial and with a few modifications I had a working code.
I used user Id as identifier and here are the parts in my code related to jwt authentication:
UserService:
public class UserService : IUserService
{
...
public IDataResult<AuthenticationResponse> Authenticate(AuthenticationRequest request)
{
UserDto userDto = _mapper.Map<UserDto>(user);
AccessToken token = generateJwtToken(userDto);
return new SuccessDataResult<AuthenticationResponse>(new AuthenticationResponse(userDto, token));
}
private AccessToken generateJwtToken(UserDto userDto)
{
AuthUser authUser = _mapper.Map<AuthUser>(userDto);
return _jwtHelper.CreateToken(authUser);
}
...
}
JWTHelper:
public class JwtHelper
{
...
public AccessToken CreateToken(AuthUser user)
{
_accessTokenExpiration = DateTime.Now.AddMinutes(_tokenOptions.ExpirationInMinutes);
var securityKey = SecurityKeyHelper.CreateSecurityKey(_tokenOptions.SecurityKey);
var signingCredentials = SigningCredentialsHelper.CreateSigningCredentials(securityKey);
var jwt = CreateJwtSecurityToken(_tokenOptions, user, signingCredentials);
var token = new JwtSecurityTokenHandler().WriteToken(jwt);
return new AccessToken
{
Token = token,
Expiration = _accessTokenExpiration
};
}
private JwtSecurityToken CreateJwtSecurityToken(TokenOptions tokenOptions, AuthUser user,
SigningCredentials signingCredentials)
{
var jwt = new JwtSecurityToken(
expires: _accessTokenExpiration,
claims: SetClaims(user),
signingCredentials: signingCredentials
);
return jwt;
}
private IEnumerable<Claim> SetClaims(AuthUser user)
{
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString());
return claims;
}
}
Startup.cs
{
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
...
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(
options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration.GetValue<string>("TokenOptions:SecurityKey")))
};
});
...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(...);
...
}
}
I've searched through some similar questions, but none helped me. I have the following situation:
Basic JWT authentication in .NET Core 2.2, Startup.cs has its settings as:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
...
var signingConfigurations = new SigningConfigurations();
services.AddSingleton(signingConfigurations);
var tokenConfigurations = new TokenConfigurations();
new ConfigureFromConfigurationOptions<TokenConfigurations>(
Configuration.GetSection("TokenConfigurations"))
.Configure(tokenConfigurations);
services.AddSingleton(tokenConfigurations);
services.AddAuthentication(authOptions =>
{
authOptions.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
authOptions.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(bearerOptions =>
{
var paramsValidation = bearerOptions.TokenValidationParameters;
paramsValidation.IssuerSigningKey = signingConfigurations.Key;
paramsValidation.ValidAudience = tokenConfigurations.Audience;
paramsValidation.ValidIssuer = tokenConfigurations.Issuer;
// Validates a received token signature
paramsValidation.ValidateIssuerSigningKey = true;
// Verifies if a received token is still valid
paramsValidation.ValidateLifetime = true;
// Tolerance time for token expiration (used if there are timezone differences)
paramsValidation.ClockSkew = TimeSpan.Zero;
});
// Activates token usage on this project
services.AddAuthorization(auth =>
{
auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser().Build());
});
Then, to generate the token:
LoginController.cs
if (validCredentials)
{
ClaimsIdentity declarations = new ClaimsIdentity(
new GenericIdentity(userInDB.UserName, "Login"),
new[] {
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("N")),
new Claim(JwtRegisteredClaimNames.UniqueName, userInDB.UserName),
new Claim("userid", userInDB.Id, ClaimValueTypes.String),
new Claim("username", userInDB.UserName, ClaimValueTypes.String),
}
);
DateTime creationDate = DateTime.Now;
DateTime expirationDate = creationDate +
TimeSpan.FromSeconds(tokenConfigurations.Seconds);
var handler = new JwtSecurityTokenHandler();
var securityToken = handler.CreateToken(new SecurityTokenDescriptor
{
Issuer = tokenConfigurations.Issuer,
Audience = tokenConfigurations.Audience,
SigningCredentials = signingConfigurations.SigningCredentials,
Subject = declarations,
NotBefore = creationDate,
Expires = expirationDate
});
var token = handler.WriteToken(securityToken);
return new
{
authenticated = true,
created = creationDate.ToString("yyyy-MM-dd HH:mm:ss"),
expiration = expirationDate.ToString("yyyy-MM-dd HH:mm:ss"),
accessToken = token,
message = "OK"
};
}
All I did so far is based on this walkthrough
So far, so good. If I add [Authorize("Bearer")] on a controller, it can be accessed only if I have the bearer on the header of the request.
But as I was implementing an UserSession structure, to intercept any request, go through the HttpContext and fill the UserSession object, it just doesn't work. The responsible method is this:
ApplicationBuilderExtensions.cs
public static IApplicationBuilder UseSessionConfiguration(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<SessionConfigurationMiddleware>();
}
public class SessionConfigurationMiddleware
{
private readonly RequestDelegate _next;
public SessionConfigurationMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context, IUserSession sessao)
{
if (context.User.Identities.Any(id => id.IsAuthenticated))
{
sessao.UserId = context.User.Claims.FirstOrDefault(x => x.Type == "userid").Value;
sessao.Roles = context.User.Claims.Where(x => x.Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/role").Select(x => x.Value).ToList();
sessao.UserName = context.User.Claims.FirstOrDefault(x => x.Type == "username").Value;
}
// Call the next delegate/middleware in the pipeline
await _next.Invoke(context);
}
}
But, as I debbug this portion of the code (which is correctly being called after any request) the IsAuthenticated property on the HttpContext is always false, in fact, everything is null, or empty on the context.User and I have no idea why.
Am I configuring the jwt wrong?
Is the middleware not correct?
Am I filling the Claims on the jwt correctly?
Why is [Authorize("Bearer")] working correctly if HttpContext is not properly filled?
Are we alone in the universe?
Thanks in advance.
I have a multiple authentication schemes to avoid the token provided by my Identity Server and Azure Active Directory
public void ConfigureServices(IServiceCollection services)
{
//...
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://localhost:5000/";
options.Audience = "api1";
options.Events = new JwtBearerEvents
{
OnTokenValidated = async context =>
{
TokenValidated(context);
},
OnAuthenticationFailed = context =>
{
context.Fail("error");
return Task.CompletedTask;
}
};
})
.AddJwtBearer(AzureADDefaults.BearerAuthenticationScheme, options =>
{
options.Audience = myAudience;
options.Authority = "https://login.microsoftonline.com/" + TenantId;
options.Events = new JwtBearerEvents
{
OnTokenValidated = async context =>
{
TokenValidated(context);
},
OnAuthenticationFailed = context =>
{
context.Fail("error");
return Task.CompletedTask;
}
};
});
}
I have an attribute for authorize the controllers of the API:
public void OnAuthorization(AuthorizationFilterContext context)
{
if (context.Filters.Any(item => item is IAllowAnonymousFilter))
{
return;
}
if (!isAuthorized(context))
{
//...
}
}
My problems is the sort of the execution. If I send a request with a token provided by ADD, at first, the request arrive at the first Bearer and go to OnAuthenticationFailed because it is not provided by IS. Then, the execution go to OnAuthorization() and then return to the second AddJwtBearer and enter on OnTokenValidated().
I need resolve the second authentication squema before the OnAuthorize of the attribute is executes.
How can I do that?
Is it possible to check the IP adress when using System.IdentityModel.Tokens.Jwt in an ASP.NET core Web Api application?
I thought about adding a Claim containing the IP of the user that requested it and check it somehow for each request. Normally I would use OnActionExecuting in ASP.NET MVC.
Is there a Middleware/Authorization based solution?
I Create my Jwt Token Claims like this:
private IEnumerable<Claim> getStandardClaims(IdentityUser user)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim("ipaddress", HttpContext.Connection.RemoteIpAddress.ToString())
};
return claims;
}
this is what the JWT Data look like:
{
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "username",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "5a6b3eb8-ed7f-48c6-b10c-a279ffd4f7c8",
"sub": "username",
"jti": "44c95b53-bfba-4f33-b4c3-834127605432",
"ipaddress": "::1",
"exp": 1542707081,
"iss": "https://localhost:5001/",
"aud": "https://localhost:5001/"
}
Edit: Possible Solution for JWT Claims?
Maybe I have to read the Claims like this (Test code, no null checks ect..):
var auth = HttpContext.Request.Headers.FirstOrDefault(x => x.Key == "Authorization");
string token = auth.Value[0].Split(' ')[1];
JwtTokenService<RefreshToken, string> jwtService = new JwtTokenService<RefreshToken, string>(null);
var principal = jwtService.GetPrincipalFromExpiredToken(token, _config["Jwt:Key"]);
Claim ipClaim = principal.FindFirst(claim => claim.Type == "ipaddress");
This is the GetPrincipalFromExpiredToken Method:
public ClaimsPrincipal GetPrincipalFromExpiredToken(string token, string securityKey)
{
var tokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey)),
ValidateLifetime = false
};
var tokenHandler = new JwtSecurityTokenHandler();
SecurityToken securityToken;
var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out securityToken);
var jwtSecurityToken = securityToken as JwtSecurityToken;
if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
throw new SecurityTokenException("Invalid token");
return principal;
}
You can do that (and all other authorization stuff) via Policy-based authorization.
public class IpCheckRequirement : IAuthorizationRequirement
{
public bool IpClaimRequired { get; set; } = true;
}
public class IpCheckHandler : AuthorizationHandler<IpCheckRequirement>
{
public IpCheckHandler(IHttpContextAccessor httpContextAccessor)
{
HttpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
}
private IHttpContextAccessor HttpContextAccessor { get; }
private HttpContext HttpContext => HttpContextAccessor.HttpContext;
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IpCheckRequirement requirement)
{
Claim ipClaim = context.User.FindFirst(claim => claim.Type == "ipaddress");
// No claim existing set and and its configured as optional so skip the check
if(ipClaim == null && !requirement.IpClaimRequired)
{
// Optional claims (IsClaimRequired=false and no "ipaddress" in the claims principal) won't call context.Fail()
// This allows next Handle to succeed. If we call Fail() the access will be denied, even if handlers
// evaluated after this one do succeed
return Task.CompletedTask;
}
if (ipClaim.Value = HttpContext.Connection.RemoteIpAddress?.ToString())
{
context.Succeed(requirement);
}
else
{
// Only call fail, to guarantee a failure, even if further handlers may succeed
context.Fail();
}
return Task.CompletedTask;
}
}
then add
services.AddSingleton<IAuthorizationHandler, IpCheckHandler>();
services.AddAuthorization(options =>
{
options.AddPolicy("SameIpPolicy",
policy => policy.Requirements.Add(new IpCheckRequirement { IpClaimRequired = true }));
});
to your ConfigureServices method.
Now you can annote the controllers on which you want to apply it with [Authroize(Policy = "SameIpPolicy")] or add a global policy:
services.AddMvc(options =>
{
options.Filters.Add(new AuthorizeFilter("SameIpPolicy"))
})
For #Tseng solution,
Set Default Policy
services.AddAuthorization(options =>
{
options.AddPolicy("SAME_IP_POLICY", i => i.Requirements.Add(new IpCheckRequirement { IpClaimRequired = true }));
options.DefaultPolicy = options.GetPolicy("SAME_IP_POLICY") ?? options.DefaultPolicy;
});
I made it work with #Tseng solution by applying [Authorize(Policy = "SameIpPolicy")] on each controller, thanks a lot man !
Just to correct one typo mistake :
Missing semi colon :
services.AddMvc(options =>
{
options.Filters.Add(new AuthorizeFilter("SameIpPolicy"));
})
ClaimsPrincipal.Current.FindFirst(claim => claim.Type == "ipaddr")
I'm using the [Authorize] attribute for authentification in my controller, but when I get a request to TestMethod I get an error: "500 Internal..".
What am I doing wrong??
That my code from StartUp.cs
services.AddAuthorization(options =>
{
options.DefaultPolicy =
new AuthorizationPolicyBuilder("Identity.Application")
.RequireAuthenticatedUser()
.Build();
});
services
.AddAuthentication(option =>
{
option.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.TokenValidationParameters =
new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
SaveSigninToken = true,
ValidateIssuer = true,
ValidIssuer = "http://blabla/",
ValidateAudience = true,
ValidAudience = "http://blabla/",
ValidateLifetime = true,
IssuerSigningKey = blabla.bla(),
ValidateIssuerSigningKey = true,
};
});
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme);
services.AddMvc();
And also code from Controller
[Route("test"), HttpPost]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public void Test() { }
Do you have ideas?
I'm using these libraries to generate the token:
System.IdentityModel.Tokens.Jwt;
Microsoft.IdentityModel.Tokens;
If you want to use the [Authorize] attribute you need to make a policy:
//new policy makes [Authorize] availible by claims
services.AddAuthorization((options) => {
options.AddPolicy("MyNewPolicy", policybuilder =>
{
policybuilder.RequireAuthenticatedUser();
policybuilder.RequireClaim("role", "someClaim");
});
});
//usage
[Authorize(Roles = "someClaim")]
public async Task<IActionResult> About(){
}
//to awnsr your comment add a list of claims to your user class ex:
new TestUser
{
SubjectId="1001",
Username="Frank",
Password="password",
Claims= new List<Claim>
{
new Claim("given_name","Frank"),
new Claim("family_name","Underwood"),
new Claim("address","1 addy rd unit 233"),
new Claim("role", "someClaim")
}
}
I ran into a lot of issues when tryout out AddJwtBearer. Finally I found out that making a manual login wasn't that much harder, worked easily and is also easier to debug.
Basically, first I created a helper class for creating and validating tokens. Here is the source code for the class: https://github.com/neville-nazerane/netcore-jwt-sample/blob/master/website/TokenGenerator.cs. Everything you had added in your TokenValidationParameters can go inside this class.
Once you have that, here is a Boiler plate authentication scheme:
public class TokenAuthenticationOptions : AuthenticationSchemeOptions
{
}
public class TokenAuthentication : AuthenticationHandler<TokenAuthenticationOptions>
{
public const string SchemeName = "TokenAuth";
public TokenAuthentication(IOptionsMonitor<TokenAuthenticationOptions> options, ILoggerFactory logger,
UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
return Task.Run(() => Authenticate());
}
private AuthenticateResult Authenticate()
{
string auth, token;
auth = Context.Request.Headers["Authorization"];
if (auth == null) return AuthenticateResult.Fail("No JWT token provided");
var auths = auth.Split(" ");
if (auths[0].ToLower() != "bearer") return AuthenticateResult.Fail("Invalid authentication");
token = auths[1];
try
{
var generator = new TokenGenerator();
var principal = generator.Validate(token);
return AuthenticateResult.Success(new AuthenticationTicket(principal, SchemeName));
}
catch
{
return AuthenticateResult.Fail("Failed to validate token");
}
}
}
Finally, in your start up you can use this scheme this way:
services.AddAuthentication(TokenAuthentication.SchemeName)
.AddScheme<TokenAuthenticationOptions, TokenAuthentication>
(TokenAuthentication.SchemeName, o => { });