I have a problem. I have in my API JWT Bearer authentication. I try to use SignalR hub with authentication but it doesn't work for me. I think I tried everything.
I have something like this:
.AddJwtBearer(conf =>
{
conf.RequireHttpsMetadata = false;
conf.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
conf.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
// THIS DOESN'T WORK - empty string
//var accessToken = context.Request.Query["access_token"];
var accessToken2 = context.Request.Headers["Authorization"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken2) &&
(path.StartsWithSegments("/DebateHub")))
{
// Read the token out of the query string
context.Token = accessToken2;
}
// return Task.CompletedTask;
return Task.FromResult<object>(null);
}
};
});
Register hub:
app.UseEndpoints(endpoints =>
{
endpoints.MapAreaControllerRoute(
name: "AreaAdmin",
areaName: "Admin",
pattern: "api/admin/{controller}/{action}");
endpoints.MapAreaControllerRoute(
name: "AreaMobile",
areaName: "Mobile",
pattern: "api/mobile/{controller}/{action}");
endpoints.MapControllers();
endpoints.MapHub<DebateHub>("/DebateHub");
endpoints.MapHub<OnlineCountHub>("/onlinecount");
});
Hub code:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class DebateHub : Microsoft.AspNetCore.SignalR.Hub
{
public override Task OnConnectedAsync()
{
string name = Context.User.Identity.Name;
Groups.AddToGroupAsync(Context.ConnectionId, name);
return base.OnConnectedAsync();
}
}
Client example:
var uri = "https://localhost:44275/DebateHub";
var connection = new HubConnectionBuilder()
.WithUrl(uri,options =>
{
options.AccessTokenProvider = () => Task.FromResult("some_token");
})
.Build();
connection.StartAsync().Wait();
It doesn't work. I still have unauthorized when I try to connect to my DebateHub. All other controllers work with my authentication ok.
What am I doing wrong?
I'm not sure but I think that you should use cookies to authorize to hub.
Look here
You must uncomment this part of your code;
//var accessToken = context.Request.Query["access_token"];
when hub connection request comes to the server it only sets the token in 'context.Request.Query', as microsoft docs states not in context.Request.Headers["Authorization"].
The query string is used on browsers when connecting with WebSockets and Server-Sent Events due to browser API limitations.
Confirm in chrome network tab request headers to see where it is being sent.
Alternatively you can use this middleware in startup configure method which dose same thing by taking the token from query and setting it where expected to be.
app.Use(async (context, next) =>
{
var accessToken = context.Request.Query["access_token"];
if (!string.IsNullOrEmpty(accessToken))
{
context.Request.Headers["Authorization"] = "Bearer " + accessToken;
}
await next.Invoke().ConfigureAwait(false);
});
Related
I have project to use SignalR from desktop application. So my application have 2 method authentication. the one is cookie for web, the one is jwt for signalr client. cause the client comes from desktop.
How to exactly authentication, i check header request, client sent
Authorization: Bearer testing
in server, i assigns the token to MessageReceivedContext.Token. so i think, it automatically handle by some magic in background by AuthorizeAttribute. the description of property Bearer Token. This will give the application an opportunity to retrieve a token from an alternative location.
Client.js
"use strict";
var connection = new signalR.HubConnectionBuilder()
.withUrl("/socket", {
accessTokenFactory: () => "testing"
})
.build();
connection.start();
Hub.cs
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class SocketHub : Hub
{
public override Task OnConnectedAsync()
{
return Task.Run(()
=> Console.WriteLine(Context.ConnectionId));
}
}
Program.cs
AddJwtBearer(options =>
{
options.Authority = "http://localhost:5000/socket";
options.RequireHttpsMetadata = false;
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Headers.Authorization;
// If the request is for our hub...
if (!string.IsNullOrEmpty(accessToken))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
I have implemented Identity Server 4 to for OAuth authentication and it is working fine when I make an authentication request in Postman (I recieve my AccessToken, Token Type, id_token, expires_in etc) and can use the access token to access my protected api. However, when I try to do this in code I end up with an error 'invalid_grant'.
Why is this working in Postman but not when I make the calls in code?
My process is as follows:
An api call is made to set the patient context by saving the patient id and a GUID to the database. The Guid is my 'launch' value.
I call a custom /auth endpoint as it requires the extra parameter ('launch') that is used maintain context ( the id value of a patient ) passing in all of the required parameters for the ID4 /connect/authorize/
eg. https://IDS4.azurewebsites.net/auth2?client_id=client&response_type=code&scope=openid
profile myAPI&client_secret=secret&state=1234567890p&aud=https://api.location.com&launch=K123K456Y7777&redirect_uri=https://test.azurewebsites.net/auth
This endpoint will associate the value of 'state' with 'launch' in the database to maintain context.
The above endpoint then calls the IDS4 /connect/authorize/ endpoint, passing in the appropriate values. In the auth pipeline I then associate 'sessionId' with 'state' again to maintain context.
The IDS4 /connect/authorize/ endpoint returns the authorization code, scope, state and session_state as expected.
Within the Get of the redirectURI specified in the 'authorize' call above, I take the authorization code and execute a standard post to the ID4 /connect/token endpoint.
The response is 'invalid_grant'
All of this works in Postman.
I have tried changing around the AllowedGrantTypes of my client but I think 'authorization_code' is the one to stick with. I use the GrantTypes enum.
The following is my Client config:
new Client
{
ClientId = "client",
ClientSecrets = { new Secret("secret".Sha256()) },
//RequireClientSecret = false, //false is default
RequirePkce = false, //to prevent 'code challenge required' message from appearing when using 'Code'
AllowedGrantTypes = GrantTypes.Code ,//{ "code", "authorization_code" },//
// where to redirect to after login
RedirectUris = { "https://IDS4.azurewebsites.net/signin-oidc", "https://test.azurewebsites.net/auth",
"https://test.azurewebsites.net/token", "https://IDS4.azurewebsites.net/Account/Login" },
// where to redirect to after logout
PostLogoutRedirectUris = { "https://IDS4.azurewebsites.net/signout-callback-oidc" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"myAPI"
}
}
My Startup code:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddDbContextPool<AppDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DBConnection")));
var builder = services.AddIdentityServer()
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients)
.AddTestUsers(TestUsers.Users)
.AddCustomTokenRequestValidator<CustomTokenRequestValidator>()
.AddCustomAuthorizeRequestValidator<CustomAuthorizeRequestValidator>();
builder.AddDeveloperSigningCredential();
services.AddAuthentication()
.AddGoogle("Google", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.ClientId = "<insert here>";
options.ClientSecret = "<insert here>";
});
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_3_0).AddXmlSerializerFormatters()
.AddMvcOptions(options => options.EnableEndpointRouting = false);
services.AddScoped<IDebugRepository, SQLDebugRepository>();
services.AddScoped<IPatientContextRepository, SQLPatientContextRepository>();
services.AddScoped<IClinicAccessRepository, SQLClinicAccessRepository>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseCookiePolicy(new CookiePolicyOptions
{
HttpOnly = HttpOnlyPolicy.None,
MinimumSameSitePolicy = SameSiteMode.None,
Secure = CookieSecurePolicy.Always
});
app.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
});
}
This is how I make the POST to the /connect/Token endpoint within the Auth Get:
[HttpGet]
public async Task<string> Get(string code, string scope, string state, string session_state)
{
try
{
//AuthResponseModel arm = new AuthResponseModel
//{
// code = code,
// scope = scope,
// state = state,
// session_state = session_state
//};
string grant_type = "authorization_code";
string redirect_uri = "https://test.azurewebsites.net/token"; //sends the token back to requestor
string client_id = "client"; //current stable testing client
string baseAddress = $"https://IDS4.azurewebsites.net/connect/token";
string client_secret = "secret";
var client = new HttpClient();
client.BaseAddress = new Uri($"https://IDS4.azurewebsites.net/");
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("client_id", client_id),
new KeyValuePair<string, string>("client_secret", client_secret),
new KeyValuePair<string, string>("grant_type", grant_type),
new KeyValuePair<string, string>("code", code),
new KeyValuePair<string, string>("redirect_uri", redirect_uri)
});
client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/x-www-form-urlencoded"));
var res = await client.PostAsync(baseAddress, content);
var resp = await res.Content.ReadAsStringAsync();
return resp;
}
catch (Exception ex)
{
return ex.Message + Environment.NewLine + ex.StackTrace ;
}
}
One thing I have not tried is setting a signing certificate. I think Postman using an internal cert that I need to accept but it's been so long I can't quite recall if I did that or not. I would think that a missing cert would have been causing me problems before now but it's something I'm looking into.
Also, just a note - I have tested the Token redirect and it is working.
Here it is:
[Controller]
[Route("Token")]
[AllowAnonymous]
public class TokenController : Controller
{
[HttpPost]
public string Post([FromForm] string access_token,
[FromForm] string token_type,
[FromForm] string expires_in,
[FromForm] string scope,
[FromForm] string patient,
[FromForm] string id_token,
[FromForm] string oceanSharedEncryptionKey)
{
TokenResponseModel token = new TokenResponseModel
{
access_token = access_token,
expires_in = expires_in,
id_token = id_token,
patient = patient,
scope = scope,
token_type = token_type
};
string rslt = JsonConvert.SerializeObject(token);
return rslt;
}
public string Get( string test1)
{
return test1;
}
}
You need to provide the grant type.
Here you can find the documentation for your case -
https://docs.identityserver.io/en/latest/topics/grant_types.html
I have the similar implementation where I use Client credentials in postman and password in the code. you can try the password in postman too
private async Task<string> GetNewAccessTokenAsync()
{
var postMessage = new Dictionary<string, string>
{
{"client_id", "Client ID goes here"},
{"client_secret","Client secret goes here" },
{"scope","User.Read" },
{"grant_type", "password"},
{"username", "Username goes here"},
{"password", "Password goes here"}
};
var client1 = new HttpClient();
var response = await client1.PostAsync("https://login.microsoftonline.com/{{id}}/oauth2/v2.0/token", new FormUrlEncodedContent(postMessage));
// return response.ReasonPhrase;
if (response.IsSuccessStatusCode)
{
var json = response.Content.ReadAsStringAsync();
return json.Result;
}
return "";
}
Please mark the answer if it is useful to you.
I wrote an ASP.NET Core 3.1 API which uses the JWTBearer authentication system. This system works well when I call the API from Postman, but I can't figure out how to call it throught my own application or ASP.NET Core 3.1 MVC Website. Here is the configuration of the API :
API Configuration :
In the Startup.cs ConfigrationServices method I added this classical piece of code :
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = true;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5),
};
});
Then I added the middleware app.UseAuthentication(); to the Configure method.
Now I have a UsersController.cs with a SignIn method which returns a JWT as string if the credentials are corrects.
Finally, I added a simple GetUsers() method with an [Authorize] tag to test the JWT authentication as following :
// GET: api/Users
[Authorize(Roles = "Administrator")]
[HttpGet]
public async Task<ActionResult<IEnumerable<User>>> GetUsers()
{
return await _context.Users.ToListAsync();
}
Throught Postman, everything works fine. I call the api/Users/SignIn url in POST passing my credentials as JSON. I get back my token in response with the 200 StatusCode.
Then I call the api/Users in GET passing the JWT previously obtained to the Postman settings Authorization > Type : Bearer Token. My API returns a successful code with all the data I asked for. Everything works as expected at the API side.
MVC Website Configuration :
To simplify the discussion with the API, I wrote a Class Library with a ClientService.cs class.
Int the ClientService.cs, I have this simplified piece of code which successfully gets the data from the API :
public async Task<string> GetPage(string model)
{
var request =
new HttpRequestMessage(
HttpMethod.Get,
_baseAddress +
model);
var client = _clientFactory.CreateClient();
var response = await client.SendAsync(request);
return await response.Content.ReadAsStringAsync();
}
If I use it, let's say to get the informations from api/something as following : var smth = await GetPage("something") or any other AllowAnonymous method, it will properly works.
But if I want to make it working with an Authorize method, I actually add this piece of code just before sending the request :
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);
Where the token variable contains my JWT hardwritten in code for testing purpose. Everything works fine too.
So now I'm obviously trying to avoid hardwritting the JWT. I decided to store it on the client side in an HttpOnly Cookie coupled with the AntiForgeryToken native function from ASP.NET Core. I wrote this code to store the cookie :
private void Authenticate(string token)
{
Response.Cookies.Append(
"JWT",
token,
new CookieOptions()
{
HttpOnly = true,
Secure = true,
}
);
}
And now I'm stucked here. Because I use it as a Service, my ClientService is the same for all my users. So I can't store the token somewhere and pass it to the newly created client for each request.
I tried to add the JWT to the header before calling the ClientService as following :
public async Task<ActionResult> Index()
{
Request.Headers.Add("Authorization", "Bearer " + Request.Cookies["JWT"]);
var users = await ClientService.GetUsersAsync();
//GetUsersAsync() simply call GetPage("users") method and deserialize the JSON returned as a List<User>
return View(users);
}
But because my ClientService create its own client which sends its own request each time I ask for some data from the API with this code :
var request =
new HttpRequestMessage(
HttpMethod.Get,,
_baseAddress +
model);
var client = _clientFactory.CreateClient();
The headers I added before aren't passed to the API.
A simple solution could be to rewrite all my ClientService methods to accept a Token parameter but it seems redundant and painful.
Which is the best and simpliest solution to pass the token to my API ?
The case you are describing is actually Cookie Authentication :
1. Sign in
ClaimsIdentity claimsIdentity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
//Not mandatory: you can add the token to claim for future usage, like API request from server to server
claimsIdentity.AddClaim(new Claim("token", tokenId));
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
await httpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal);
2. Add Cookie authentication
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = true;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5),
};
}).AddCookieAuthentication();
AddCookieAuthentication looks like:
public static AuthenticationBuilder AddCookieAuthentication(this IServiceCollection services)
{
var authBuilder = services.AddAuthentication(sharedOptions =>
{
sharedOptions.RequireAuthenticatedSignIn = false;
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, opts =>
{
opts.CookieManager = new ChunkingCookieManager();
opts.Cookie = new CookieBuilder()
{
Domain = "CookieDomain",
Name = "CookieName",
Path = "CookiePath",
SecurePolicy = CookieSecurePolicy.Always,
HttpOnly = true,
SameSite = SameSiteMode.Lax,
};
opts.ExpireTimeSpan = TimeSpan.FromMinutes(20);
opts.ForwardDefaultSelector = ctx =>
{
var authHeader = ctx.Request.Headers["Authorization"].FirstOrDefault();
if (authHeader?.StartsWith(JwtBearerDefaults.AuthenticationScheme) == true)
{
return JwtBearerDefaults.AuthenticationScheme;
}
else
{
return CookieAuthenticationDefaults.AuthenticationScheme;
}
};
});
return authBuilder;
}
A browser through which the user signed-in will receive the cookie cookie created in SignIn method and will be able to populate request via client as well.
Ok I figured it out myself, here is how I solved the problem :
1) On Startup.cs, add an HttpContextAccessor :
The Context from our ASP.NET Core website is only accessible by the controllers. The Class libraries can't access it.
To make it available to a class library, we need to use an HttPContextAccessor. It's a service we can use as a dependancy injection to our class library. We just have to add
services.AddHttpContextAccessor();
In the ConfigureServices method from the Startup.cs.
2) Adapt the Class Library
The ClientService needs to be able to receive the Dependancy Injection as following :
public class ClientService : IClientService
{
private readonly IHttpClientFactory _clientFactory;
private readonly IHttpContextAccessor _contextAccessor;
private readonly string _baseAddress = $#"https://localhost:[PORT]/api/";
public ClientService(IHttpClientFactory clientFactory, IHttpContextAccessor contextAccessor)
{
_clientFactory = clientFactory;
_contextAccessor = contextAccessor;
}
}
Now we can access the current context from the HttpContextAccessor _contextAccessor variable.
Next we have to modify our GetPage(string model) method :
public async Task<string> GetPage(string model)
{
var request =
new HttpRequestMessage(
HttpMethod.Get,
_baseAddress +
model);
var client = _clientFactory.CreateClient();
var authorization = _contextAccessor.HttpContext.Request.Headers.FirstOrDefault(x => x.Key == "Authorization").Value.FirstOrDefault();
if(authorization != null)
{
client.DefaultRequestHeaders.Add("Authorization", authorization);
}
var response = await client.SendAsync(request);
return await response.Content.ReadAsStringAsync();
}
The code added will try to get the "Authorization" header from the request and if it's not null, we add it to our own request. This allows the request to the API with the JWT.
3) Add a middleware to add the JWT to each request
Now the final step is to add the JWT to each request from our ASP.NET Core application. If needed, the JWT will be consumed by our Class Library without changing any piece of code, either from the library or from the application.
In order to realise that, we have to add a middleware to the Startup.cs > Configure() method.
The middleware looks like that :
// Add header:
app.Use((context, next) =>
{
var jwt = context.Request.Cookies["JWT"];
if(jwt != null)
{
context.Request.Headers.Add("Authorization", "Bearer " + jwt);
}
return next.Invoke();
});
This just reads the Cookie from the request headers and if the JWT HttpOnly Cookie isn't null, we add the token to our request. This request will then be captured by our Class Library in order to use it to talk with the API.
A few things to note :
Adding the JWT to each request even if we don't need it could consume more bandwidth overtime. It could be also less safe (I'm not a security expert)
Adding the JWT to our main request, then create a local client with its own request in our class library seems not optimal. But it works as expected and I'm actually unaware of how to optimize it.
Feel free to point any issue in my solution or modify it to add some clarifications or even other more efficient ways to achieve it.
I have an ASP.NET Core app that I am integrating with Auth0. After the authentication, I want to redirect to a page to collect information to create a local account, just like the default Facebook and Google extensions do.
I set up a main cookie, an external cookie and my Auth0 point. It then does a callback to the page (/Account/ExternalLogin), where I sign in to the main cookie after doing whatever they need to do, and redirect to a page that requires authorization (/Profile. This all works fine.
However, if I just try to go to that page rather than via the Login route, i get stuck in a loop.
I'm quite sure I'm missing just one stupid thing, but can't seem to get it.
I've tried to pretty much every combination of things I can figure out and have hit the wall. I'm sure it's something stupid.
Here's my relevant part of startup.cs
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
// Add authentication services
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "MainCookie";
options.DefaultChallengeScheme = "Auth0";
})
.AddCookie("MainCookie", options =>
{
options.ForwardChallenge = "Auth0";
})
.AddCookie("External", options =>
{
})
.AddOpenIdConnect("Auth0", options =>
{
// Set the authority to your Auth0 domain
options.Authority = $"https://{Configuration["Auth0:Domain"]}";
// Configure the Auth0 Client ID and Client Secret
options.ClientId = Configuration["Auth0:ClientId"];
options.ClientSecret = Configuration["Auth0:ClientSecret"];
// Set response type to code
options.ResponseType = "code";
// Configure the scope
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.SignInScheme = "External";
// Set the callback path, so Auth0 will call back to http://localhost:3000/callback
// Also ensure that you have added the URL as an Allowed Callback URL in your Auth0 dashboard
options.CallbackPath = new PathString("/callback");
// Configure the Claims Issuer to be Auth0
options.ClaimsIssuer = "Auth0";
options.Events = new OpenIdConnectEvents
{
// handle the logout redirection
OnRedirectToIdentityProviderForSignOut = (context) =>
{
var logoutUri = $"https://{Configuration["Auth0:Domain"]}/v2/logout?client_id={Configuration["Auth0:ClientId"]}";
var postLogoutUri = context.Properties.RedirectUri;
if (!string.IsNullOrEmpty(postLogoutUri))
{
if (postLogoutUri.StartsWith("/"))
{
// transform to absolute
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;
}
};
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizePage("/Profile");
});
}
Here's AccountController
public class AccountController : Controller
{
public async Task Login(string returnUrl = "/")
{
var redirectUrl = Url.Page("/ExternalLogin", pageHandler: "Callback", values: new { returnUrl });
await HttpContext.ChallengeAsync("Auth0", new AuthenticationProperties() { RedirectUri = redirectUrl });
}
[Authorize]
public async Task Logout()
{
await HttpContext.SignOutAsync("External");
await HttpContext.SignOutAsync("MainCookie");
await HttpContext.SignOutAsync("Auth0", new AuthenticationProperties
{
RedirectUri = Url.Action("Index", "Home")
});
}
}
SO we redirect to ExternalLogin callback. Currently there is just a submit button that goes to the Confirm callback which completes the login. This will eventually be replaced with a check to see if I have an account for them, and force them to register.
public class ExternalLoginModel : PageModel
{
public IActionResult OnPost(string provider, string returnUrl = null)
{
var redirectUrl = Url.Page("./ExternalLogin", pageHandler: "Callback", values: new { returnUrl });
return new ChallengeResult(provider, null);
}
public async Task<IActionResult> OnGetCallbackAsync(string returnUrl = null, string remoteError = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
if (remoteError != null)
{
ErrorMessage = $"Error from external provider: {remoteError}";
return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
}
return Page();
}
public async Task<IActionResult> OnPostConfirmAsync()
{
var claimsPrincipal = await HttpContext.AuthenticateAsync("External");
await HttpContext.SignInAsync("MainCookie", claimsPrincipal.Principal);
await HttpContext.SignOutAsync("External");
return RedirectToPage("/Profile");
}
}
So when I go /Account/Login, it correctly sends me to Auth0, then to ExternalLogin, and I can click the button and set the Main Cookie. This then lets me access /Profile.
However, If I'm not already authorized, If I execute /Profile, I then kick over to Auth0, but after authenticating I just get stuck in a loop like this.
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/2.0 GET https://localhost:44375/profile
Microsoft.AspNetCore.Routing.EndpointMiddleware:Information: Executing endpoint 'Page: /Profile'
Microsoft.AspNetCore.Mvc.RazorPages.Internal.PageActionInvoker:Information: Route matched with {page = "/Profile", action = "", controller = ""}. Executing page /Profile
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService:Information: Authorization failed.
Microsoft.AspNetCore.Mvc.RazorPages.Internal.PageActionInvoker:Information: Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.
Microsoft.AspNetCore.Mvc.ChallengeResult:Information: Executing ChallengeResult with authentication schemes ().
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler:Information: AuthenticationScheme: Auth0 was challenged.
Microsoft.AspNetCore.Mvc.RazorPages.Internal.PageActionInvoker:Information: Executed page /Profile in 11.2594ms
Microsoft.AspNetCore.Routing.EndpointMiddleware:Information: Executed endpoint 'Page: /Profile'
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 28.548ms 302
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/2.0 POST https://localhost:44375/callback application/x-www-form-urlencoded 375
Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler:Information: AuthenticationScheme: External signed in.
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 113.1223ms 302
Changing options.DefaultChallengeScheme = "Auth0" to options.DefaultChallengeScheme = "MainCookie" was all that was needed.
we have a SPA (Angular) with API backend (ASP.NET Core WebAPI):
SPA is listens on app.mydomain.com, API on app.mydomain.com/API
We use JWT for Authentication with built-in Microsoft.AspNetCore.Authentication.JwtBearer; I have a controller app.mydomain.com/API/auth/jwt/login which creates tokens. SPA saves them into local storage. All works perfect. After a security audit, we have been told to switch local storage for cookies.
The problem is, that API on app.mydomain.com/API is used by SPA but also by a mobile app and several customers server-2-server solutions.
So, we have to keep JWT as is, but add Cookies. I found several articles which combines Cookies and JWT on different controllers, but I need them work side-by-side on each controller.
If client sends cookies, authenticate via cookies. If client sends JWT bearer, authenticate via JWT.
Is this achievable via built-in ASP.NET authentication or DIY middleware?
Thanks!
Okay, I have been trying achieving this for a while and i solved same issue of using jwt Authentication Tokens and Cookie Authentication with the following code.
API Service Provider UserController.cs
This Provide Different Services to the User with Both (Cookie and JWT Bearer)Authentication Schemes
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
[Route("[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
private readonly IUserServices_Api _services;
public UsersController(IUserServices_Api services)
{
this._services = services;
}
[HttpGet]
public IEnumerable<User> Getall()
{
return _services.GetAll();
}
}
My Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddAuthentication(options => {
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/Account/Login";
options.AccessDeniedPath = "/Home/Error";
})
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidAudience = " you site link blah blah",
ValidIssuer = "You Site link Blah blah",
IssuerSigningKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(sysController.GetSecurityKey()))
,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
});
}
And further if you want custom Authentication for a specific Controller
then you have to specify the Authentitcation Type for the Authorization
like:
[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
public IActionResult Index()
{
return View(); // This can only be Access when Cookie Authentication is Authorized.
}
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public IActionResult Index()
{
return View(); // And this one will be Access when JWT Bearer is Valid
}
I've been having the same issue and i just found what it seems to be the solution in another question here in stackoverflow.
Please take a look at this.
I'll try that solution myself and update this answer with the results.
Edit: It seems it's not possible to achieve double authentication types in a same method but the solution provided in the link i mentioned says:
It's not possible to authorize a method with two Schemes Or-Like, but you can use two public methods, to call a private method
//private method
private IActionResult GetThingPrivate()
{
//your Code here
}
//Jwt-Method
[Authorize(AuthenticationSchemes = $"{JwtBearerDefaults.AuthenticationScheme}")]
[HttpGet("bearer")]
public IActionResult GetByBearer()
{
return GetThingsPrivate();
}
//Cookie-Method
[Authorize(AuthenticationSchemes = $"{CookieAuthenticationDefaults.AuthenticationScheme}")]
[HttpGet("cookie")]
public IActionResult GetByCookie()
{
return GetThingsPrivate();
}
Anyway you should take a look at the link, it sure helped me.
Credit goes to Nikolaus for the answer.
I have not been able to find much information on a good way to do this - having to duplicate the API is a pain just to support 2 authorization schemes.
I have been looking into the idea of using a reverse proxy and it looks to me like a good solution for this.
User signs into Website (use cookie httpOnly for session)
Website uses Anti-Forgery token
SPA sends request to website server and includes anti-forgery token in header: https://app.mydomain.com/api/secureResource
Website server verifies anti-forgery token (CSRF)
Website server determines request is for API and should send it to the reverse proxy
Website server gets users access token for API
Reverse proxy forwards request to API: https://api.mydomain.com/api/secureResource
Note that the anti-forgery token (#2,#4) is critical or else you could expose your API to CSRF attacks.
Example (.NET Core 2.1 MVC with IdentityServer4):
To get a working example of this I started with the IdentityServer4 quick start Switching to Hybrid Flow and adding API Access back. This sets up the scenario I was after where a MVC application uses cookies and can request an access_token from the identity server to make calls the API.
I used Microsoft.AspNetCore.Proxy for the reverse proxy and modified the quick start.
MVC Startup.ConfigureServices:
services.AddAntiforgery();
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
MVC Startup.Configure:
app.MapWhen(IsApiRequest, builder =>
{
builder.UseAntiforgeryTokens();
var messageHandler = new BearerTokenRequestHandler(builder.ApplicationServices);
var proxyOptions = new ProxyOptions
{
Scheme = "https",
Host = "api.mydomain.com",
Port = "443",
BackChannelMessageHandler = messageHandler
};
builder.RunProxy(proxyOptions);
});
private static bool IsApiRequest(HttpContext httpContext)
{
return httpContext.Request.Path.Value.StartsWith(#"/api/", StringComparison.OrdinalIgnoreCase);
}
ValidateAntiForgeryToken (Marius Schulz):
public class ValidateAntiForgeryTokenMiddleware
{
private readonly RequestDelegate next;
private readonly IAntiforgery antiforgery;
public ValidateAntiForgeryTokenMiddleware(RequestDelegate next, IAntiforgery antiforgery)
{
this.next = next;
this.antiforgery = antiforgery;
}
public async Task Invoke(HttpContext context)
{
await antiforgery.ValidateRequestAsync(context);
await next(context);
}
}
public static class ApplicationBuilderExtensions
{
public static IApplicationBuilder UseAntiforgeryTokens(this IApplicationBuilder app)
{
return app.UseMiddleware<ValidateAntiForgeryTokenMiddleware>();
}
}
BearerTokenRequestHandler:
public class BearerTokenRequestHandler : DelegatingHandler
{
private readonly IServiceProvider serviceProvider;
public BearerTokenRequestHandler(IServiceProvider serviceProvider, HttpMessageHandler innerHandler = null)
{
this.serviceProvider = serviceProvider;
InnerHandler = innerHandler ?? new HttpClientHandler();
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var httpContextAccessor = serviceProvider.GetService<IHttpContextAccessor>();
var accessToken = await httpContextAccessor.HttpContext.GetTokenAsync("access_token");
request.Headers.Authorization =new AuthenticationHeaderValue("Bearer", accessToken);
var result = await base.SendAsync(request, cancellationToken);
return result;
}
}
_Layout.cshtml:
#Html.AntiForgeryToken()
Then using your SPA framework you can make a request. To verify I just did a simple AJAX request:
<a onclick="sendSecureAjaxRequest()">Do Secure AJAX Request</a>
<div id="ajax-content"></div>
<script language="javascript">
function sendSecureAjaxRequest(path) {
var myRequest = new XMLHttpRequest();
myRequest.open('GET', '/api/secureResource');
myRequest.setRequestHeader("RequestVerificationToken",
document.getElementsByName('__RequestVerificationToken')[0].value);
myRequest.onreadystatechange = function () {
if (myRequest.readyState === XMLHttpRequest.DONE) {
if (myRequest.status === 200) {
document.getElementById('ajax-content').innerHTML = myRequest.responseText;
} else {
alert('There was an error processing the AJAX request: ' + myRequest.status);
}
}
};
myRequest.send();
};
</script>
This was a proof of concept test so your mileage may very and I'm pretty new to .NET Core and middleware configuration so it could probably look prettier. I did limited testing with this and only did a GET request to the API and did not use SSL (https).
As expected, if the anti-forgery token is removed from the AJAX request it fails. If the user is has not logged in (authenticated) the request fails.
As always, each project is unique so always verify your security requirements are met. Please take a look at any comments left on this answer for any potential security concerns someone might raise.
On another note, I think once subresource integrity (SRI) and content security policy (CSP) is available on all commonly used browsers (i.e. older browsers are phased out) local storage should be re-evaluated to store API tokens which will lesson the complexity of token storage. SRI and CSP should be used now to help reduce the attack surface for supporting browsers.
I think the easiest solution is one proposed by David Kirkland:
Create combined authorization policy (in ConfigureServices(IServiceCollection services)):
services.AddAuthorization(options =>
{
var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
CookieAuthenticationDefaults.AuthenticationScheme,
JwtBearerDefaults.AuthenticationScheme);
defaultAuthorizationPolicyBuilder =
defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});
And add middleware that will redirect to login in case of 401 (in Configure(IApplicationBuilder app)):
app.UseAuthentication();
app.Use(async (context, next) =>
{
await next();
var bearerAuth = context.Request.Headers["Authorization"]
.FirstOrDefault()?.StartsWith("Bearer ") ?? false;
if (context.Response.StatusCode == 401
&& !context.User.Identity.IsAuthenticated
&& !bearerAuth)
{
await context.ChallengeAsync("oidc");
}
});
while looking for combined firebase authorization with net core web api (cookie for web site and authorization header for mobile app ) end with the following solution.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://securetoken.google.com/xxxxx";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = options.Authority,
ValidateAudience = true,
ValidAudience = "xxxxx",
ValidateLifetime = true
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
if (context.Request.Cookies.ContainsKey(GlobalConst.JwtBearer))
{
context.Token = context.Request.Cookies[GlobalConst.JwtBearer];
}
else if (context.Request.Headers.ContainsKey("Authorization"))
{
var authhdr = context.Request.Headers["Authorization"].FirstOrDefault(k=>k.StartsWith(GlobalConst.JwtBearer));
if (!string.IsNullOrEmpty(authhdr))
{
var keyval = authhdr.Split(" ");
if (keyval != null && keyval.Length > 1) context.Token = keyval[1];
}
}
return Task.CompletedTask;
}
};
});
where
public static readonly string JwtBearer = "Bearer";
seems working fine.
checked it from mobile & postman (for cookie )
with this code you can use cookie and header in the same time.
if cookie is null then check the header automatically.
add this code in AddJwtBearer options.
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
context.Token = context.Request.Cookies["Authorization"];
return Task.CompletedTask;
}
};
full usage is:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidAudience = Configuration["JwtToken:Audience"],
ValidIssuer = Configuration["JwtToken:Issuer"],
IssuerSigningKey =
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtToken:Key"]))
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
context.Token = context.Request.Cookies["Authorization"];
return Task.CompletedTask;
}
};
});
Header => Authorization: Bearer Your-Token
or
Cookie => Authorization=Your-Token
//dont add Bearer in Cookie
I found this nice article by Rick Strahl. It is the best solution that I found so far and I used it in .NET 5
Here is the key code in order to combining JWT token and cookie authentication in .NET applications:
services.AddAuthentication(options =>
{
options.DefaultScheme = "JWT_OR_COOKIE";
options.DefaultChallengeScheme = "JWT_OR_COOKIE";
})
.AddCookie( options =>
{
options.LoginPath = "/login";
options.ExpireTimeSpan = TimeSpan.FromDays(1);
})
.AddJwtBearer( options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = config.JwtToken.Issuer,
ValidateAudience = true,
ValidAudience = config.JwtToken.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.JwtToken.SigningKey))
};
})
.AddPolicyScheme("JWT_OR_COOKIE", "JWT_OR_COOKIE", options =>
{
options.ForwardDefaultSelector = context =>
{
string authorization = context.Request.Headers[HeaderNames.Authorization];
if (!string.IsNullOrEmpty(authorization) && authorization.StartsWith("Bearer "))
return JwtBearerDefaults.AuthenticationScheme;
return CookieAuthenticationDefaults.AuthenticationScheme;
};
});
ASP.NET Core 2.0 Web API
Please follow this post for implementing JWT Token based authentication
https://fullstackmark.com/post/13/jwt-authentication-with-aspnet-core-2-web-api-angular-5-net-core-identity-and-facebook-login
If you are using visual studio make sure apply the Bearer type athentication type with the filter
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
for controller or actions.