Multiple JWT Tokens and Multiple Auth Handlers - asp.net-core

I currently have Auth in place for my API application using a JWT token with Identity User, Which works great, I am now trying to chain on another JWT Token which users keycloak and I want to setup A custom auth Handler just for that token type, is that possible? My Code looks as follows:
services.AddAuthentication()
.AddJwtBearer("KeyCloak", opt =>
{
opt.Authority = "site.co.za/auth/realms/Development";
opt.Audience = "dev";
opt.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidAudiences = new string[] { "dev" },
NameClaimType = "preferred_username",
RoleClaimType = "role"
};
opt.Events = new JwtBearerEvents()
{
OnAuthenticationFailed = c =>
{
c.NoResult();
c.Response.StatusCode = 500;
c.Response.ContentType = "text/plain";
return c.Response.WriteAsync(c.Exception.ToString());
}
};
opt.RequireHttpsMetadata = false;
opt.SaveToken = true;
opt.Validate();
})
.AddJwtBearer("Internal", opt =>
{
opt.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(Constants.AuthTokenKey))
};
});
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("KeyCloak", "Internal").Build();
});
I need to add a custom Auth HandleAuthenticateAsync just for keycloak and use the normal out of the box HandleAuthenticateAsync for internal. Is this possible to do?

You can do this:
this will select the jwt token validation based on API path.
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = "Custom";
})
.AddPolicyScheme("Custom", "Custom", options =>
{
options.ForwardDefaultSelector = context =>
{
bool isKeyCloakAuthRequired = context.Request.Path.StartsWithSegments("/apithatneedskeycloakauth");
if (isKeyCloakAuthRequired)
{
return "Keycloak";
}
else
{
return "Internal";
}
};
})
.AddJwtBearer("Keycloak", options =>
{
// your code for keycloak validation parameters.
})
.AddJwtBearer("Internal", options =>
{
// your code for token validation parameters.
})
Hope it helps.

Related

How to prefer one authentication scheme over another?

I have two JwtBearer schemes, each one can use cookie in addition to authorization header
.AddJwtBearer(OAuthSchemeConstants.SchemeName, options =>
{
options.Authority = oauthServerUrl;
options.AutomaticRefreshInterval = new TimeSpan(24, 0, 0);
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = true,
ValidIssuer = oauthServerUrl,
ClockSkew = TimeSpan.Zero,
NameClaimType = AuthenticationConstants.NameClaimType,
};
options.Events = new JwtBearerEvents()
{
OnMessageReceived = (context) =>
{
var authHeader = context.Request.GetAuthorizationHeader();
var authCookie = context.Request.GetCookie(OAuthSchemeConstants.CookieName);
if (string.IsNullOrEmpty(authHeader) && !string.IsNullOrEmpty(authCookie))
{
context.Token = authCookie;
}
return Task.CompletedTask;
},
}
});
.AddJwtBearer(CustomSchemeConstants.SchemeName, options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey,
ValidateIssuer = false,
ValidateAudience = false,
ClockSkew = TimeSpan.Zero,
NameClaimType = AuthenticationConstants.NameClaimType
};
options.Events = new JwtBearerEvents()
{
OnMessageReceived = (context) =>
{
var authHeader = context.Request.GetAuthorizationHeader();
var authCookie = context.Request.GetCookie(CustomSchemeConstants.CookieName);
if (string.IsNullOrEmpty(authHeader) && !string.IsNullOrEmpty(authCookie))
{
context.Token = authCookie;
}
return Task.CompletedTask;
}
};
});
When cookies for both schemes are present, I want to prefer OAuthSchemeConstants.SchemeName over CustomSchemeConstants.SchemeName.
My default authorization policy looks like this:
var policyBuilder = new AuthorizationPolicyBuilder(
OAuthSchemeConstants.SchemeName,
CustomSchemeConstants.SchemeName
);
policyBuilder = policyBuilder.RequireAuthenticatedUser();
return policyBuilder.Build();
What I tried:
setting options.DefaultAuthenticationScheme / options.DefaultChallengeScheme to OAuthSchemeConstants.SchemeName in .AddAuthentication()
change order in which schemes are added to authentication builder
change order of schemes in AuthorizationPolicyBuilder
But no matter what, when cookies for both schemes are present, CustomSchemeConstants.SchemeName is being used for authentication.
I need this because I am migrating authentication scheme to OAuthSchemeConstants.SchemeName and I need both schemes to be working but prefer OAuthSchemeConstants.SchemeName over CustomSchemeConstants.SchemeName.
CustomSchemeConstants.SchemeName is just "MyCustomScheme" and OAuthSchemeConstants.SchemeName is just "MyOAuthScheme".
So when both schemes are "valid" (valid jwts in cookies are present for both schemes) how I can control which scheme is being used for authentication?

How to return a specific status code when using JWT

I am using .NET Core
The middleware seems to magically let me set up authentication and this is my code
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddCookie(option =>
{
option.LoginPath = "/account/";
// option.LogoutPath = "/account/logout";
})
.AddJwtBearer(jwtBearerOptions =>
{
jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"])),
ClockSkew = TimeSpan.Zero
};
}
);
The problem I have, is when the user calls an end point and is not authenticated the browser displays a prompt for the user to enter their username and password.
From what I've read, this is because of the response 401.
I'd like to change the response code from 401 to 400 (or any number)
I don't know how. I can't see any options within the AddAuthentication
Using HttpResponse-OnStarting method in JwtBearerEvents-OnAuthenticationFailed property:
jwtBearerOptions.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
context.Response.OnStarting(() =>
{
context.Response.StatusCode = 400;
return Task.CompletedTask;
});
return Task.CompletedTask;
}
};

Audience parameter is missing from Bearer Token while using Swagger (swashbuckle v5.3.1) with Asp.net Core 3.1.1

Jwt Authentication section from Startup.cs:
services.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Authority = config["Authority"];
options.RequireHttpsMetadata = Convert.ToBoolean(config["RequireHttpsMetadata"]);
options.Authority = config["Authority"];
options.Audience = config["Audience"];
options.TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = config["Audience"],
ValidateAudience = Convert.ToBoolean(config["ValidateAudience"]),
ValidateIssuer = Convert.ToBoolean(config["ValidateIssuer"]),
};
});
Swagger configuration:
services.AddSwaggerGen(setup => {
setup.SwaggerDoc("1.0", new OpenApiInfo
{
Title = "Switchboard Live Cloud API v1.0",
Version = "1.0",
Description = "Programmable access to Switchboard Live's Cloud Platform.",
});
setup.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
Implicit = new OpenApiOAuthFlow
{
AuthorizationUrl = new System.Uri(string.Format("{0}/connect/authorize", authority)),
Scopes = new Dictionary<string, string> {
{ "read", "R" },
{ "write", "W" }
}
}
}
});
setup.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" }
},
new[] { "read", "write" }
}
});
.....
app.UseSwagger();
app.UseSwaggerUI(setup => {
setup.SwaggerEndpoint("/swagger/1.0/swagger.json", "Title");
setup.RoutePrefix = string.Empty;
});
I don't see what am I doing wrong here, but when I start the application, check the scopes and go through the authorization process, I end up with a Bearer Token that do NOT have the audience field encrypted in it, so all my requests ends up with a 401 Unauthorized response and the following header error:
www-authenticate: Bearer error="invalid_token", error_description="The audience 'empty' is invalid"
Any suggestions/solutions for this?
There was a major change in IdentityServer4 version v4 they are no longer setting the aud claim by default.
If you check the configuration section of the official documentation it says you need to disable the aud Claim:
https://identityserver4.readthedocs.io/en/latest/quickstarts/1_client_credentials.html#configuration
So, you need set the property ValidateAudience = false with something like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://localhost:5001";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
}

Reading JWT Token from API in ASP.NET Core

My setup: I've created and have running a WebAPI solution that performs the authentication of a username and password against a source (currently a db). This generates the JWT token and returns it to the requesting app (a ASP.NET Core 2.2 app).
Most solutions talk of securing the WebAPI exposed methods but my approach is to only do the authentication through WebAPI. The individual apps need to accept the token so they can determine authorization.
Now the question: what is the best approach to reading the token from the WebAPI (which I've done already), validating it, and then storing it for any/all controllers to know there is an authenticated user (via Authorize attribute) so long as the token is valid?
Debugging this more, it seems my token is not being added to the headers. I see this debug message:
Authorization failed for the request at filter 'Microsoft.AspNet.Mvc.Filters.AuthorizeFilter'
Code Update2 - code that gets the JWT:
var client = _httpClientFactory.CreateClient();
client.BaseAddress = new Uri(_configuration.GetSection("SecurityApi:Url").Value);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
//login
Task<HttpResponseMessage> response = ValidateUserAsync(client, username, password);
Task<Core.Identity.TokenViewModel> tokenResult = response.Result.Content.ReadAsAsync<Core.Identity.TokenViewModel>();
if (!response.Result.IsSuccessStatusCode)
{
if (tokenResult != null && tokenResult.Result != null)
{
ModelState.AddModelError("", tokenResult.Result.ReasonPhrase);
}
else
{
ModelState.AddModelError("", AppStrings.InvalidLoginError);
}
return View();
}
JwtSecurityToken token = new JwtSecurityToken(tokenResult.Result.Token);
int userId;
if (int.TryParse(token.Claims.First(s => s.Type == JwtRegisteredClaimNames.NameId).Value, out userId))
{
//load app claims
Core.Identity.UserInfo userInfo = Core.Identity.UserLogin.GetUser(_identityCtx, userId);
Core.Identity.UserStore uStore = new Core.Identity.UserStore(_identityCtx);
IList<Claim> claims = uStore.GetClaimsAsync(userInfo, new System.Threading.CancellationToken(false)).Result;
claims.Add(new Claim(Core.Identity.PowerFleetClaims.PowerFleetBaseClaim, Core.Identity.PowerFleetClaims.BaseUri));
ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme);
ClaimsPrincipal principal = new ClaimsPrincipal(claimsIdentity);
//complete
AuthenticationProperties authProperties = new AuthenticationProperties();
authProperties.ExpiresUtc = token.ValidTo;
authProperties.AllowRefresh = false;
authProperties.IsPersistent = true;
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(JwtBearerDefaults.AuthenticationScheme, tokenResult.Result.Token);
//var stuff = HttpContext.SignInAsync(JwtBearerDefaults.AuthenticationScheme, principal, authProperties);
}
else
{
ModelState.AddModelError("", AppStrings.InvalidLoginError);
return View();
}
return RedirectToAction("Index", "Home");
Startup:
private void ConfigureIdentityServices(IServiceCollection services)
{
services.ConfigureApplicationCookie(options => options.LoginPath = "/Login");
//authentication token
services.AddAuthentication(opt =>
{
opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddCookie(opt =>
{
opt.LoginPath = "/Login";
opt.LogoutPath = "/Login/Logoff";
opt.Cookie.Name = Configuration.GetSection("SecurityApi:CookieName").Value;
}).AddJwtBearer(options =>
{
options.SaveToken = true;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateAudience = true,
ValidAudience = Configuration.GetSection("SecurityApi:Issuer").Value,
ValidateIssuer = true,
ValidIssuer = Configuration.GetSection("SecurityApi:Issuer").Value,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration.GetSection("SecurityApi:Key").Value)),
ValidateLifetime = true
};
});
Core.Startup authStart = new Core.Startup(this.Configuration);
authStart.ConfigureAuthorizationServices(services);
}
Auth:
public void ConfigureAuthorizationServices(IServiceCollection services)
{
services.AddDbContext<Identity.IdentityContext>(options => options.UseSqlServer(Configuration.GetConnectionString("SecurityConn")));
services.AddScoped<DbContext, Identity.IdentityContext>(f =>
{
return f.GetService<Identity.IdentityContext>();
});
services.AddIdentityCore<Identity.UserInfo>().AddEntityFrameworkStores<Identity.IdentityContext>().AddRoles<Identity.Role>();
services.AddTransient<IUserClaimStore<Core.Identity.UserInfo>, Core.Identity.UserStore>();
services.AddTransient<IUserRoleStore<Core.Identity.UserInfo>, Core.Identity.UserStore>();
services.AddTransient<IRoleStore<Core.Identity.Role>, Core.Identity.RoleStore>();
services.AddAuthorization(auth =>
{
auth.AddPolicy(JwtBearerDefaults.AuthenticationScheme, new AuthorizationPolicyBuilder().AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme).RequireAuthenticatedUser().Build());
auth.AddPolicy(PFBaseClaim, policy => policy.RequireClaim(Identity.PFClaims.BaseUri));
});
}
In the end, my approach was to use a secure cookie and a base claim to prove the user authenticated.
private void ConfigureAuthentication(IServiceCollection services)
{
services.ConfigureApplicationCookie(options => options.LoginPath = "/Login");
//authentication token
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(opt =>
{
opt.LoginPath = "/Login";
opt.AccessDeniedPath = "/Login";
opt.LogoutPath = "/Login/Logoff";
opt.Cookie.Name = Configuration.GetSection("SecurityApi:CookieName").Value;
}).AddJwtBearer(options =>
{
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateAudience = true,
ValidAudience = Configuration.GetSection("SecurityApi:Issuer").Value,
ValidateIssuer = true,
ValidIssuer = Configuration.GetSection("SecurityApi:Issuer").Value,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration.GetSection("SecurityApi:Key").Value)),
ValidateLifetime = true
};
});
}
And at login:
AuthenticationProperties authProperties = new AuthenticationProperties();
authProperties.ExpiresUtc = token.ValidTo;
authProperties.AllowRefresh = false;
authProperties.IsPersistent = true;
HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, userStore.CreateAsync(user).Result, authProperties);
return RedirectToAction("Index", "Home");

Asp.Net Core & JWT authentication: How to know authentication failed because token expired?

Below is the JWT authentication I am using:
.AddJwtBearer(options =>
{
// options.SaveToken = false;
// options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(AuthConfig.GetSecretKey(Configuration)),
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
};
options.Events = new JwtBearerEvents()
{
OnChallenge = c =>
{
c.HandleResponse();
// TODO: How to know if the token was expired?
return AspNetUtils.WriteJsonAsync(c.Response, new Result<string>
{
Message = "Unauthenticated.",
IsError = true
}, 401);
},
};
});
The authentication is working fine. For new requirements, I need to know if authentication failed because the JWT token was expired or not.
Note that authentication failed because of multi reasons. The token can be missing, tampered, or expired.
Any ideas? Thanks!
.AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents()
{
OnAuthenticationFailed = context =>
{
if(context.Exception is SecurityTokenExpiredException)
{
// if you end up here, you know that the token is expired
}
}
};
})
Using OnChallenge property:
.AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents
{
OnChallenge = context =>
{
if (context?.AuthenticateFailure is SecurityTokenExpiredException)
{
var error = context.Error; // "invalid_token"
var errorDescription = context.ErrorDescription; // "The token is expired"
}
return Task.CompletedTask;
}
};
});