OpenID unable to get ID_token in .Net Core - .net-core

I am using OpenID to get ID_token/ accesstoken. The problem is in OpenID workflow I am able to get authorization code from identity server but next step is to use the authorisation code and call the oauth/token endpoint to get the access token.
In order to call the Oauth/token endpoint , I need to pass the client ID and client secret as Request header(basic authentication) but it is getting passed in the request body, how do I get to pass it in the header instead of request body??
Here is the sample code from Startup.cs (.Net core 3.1)
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.Events = new CookieAuthenticationEvents()
{
OnSigningIn = async context =>
{
var scheme = context.Properties.Items.Where(k => k.Key == ".AuthScheme").FirstOrDefault();
var claim = new Claim(scheme.Key, scheme.Value);
var claimsIdentity = context.Principal.Identity as ClaimsIdentity;
claimsIdentity.AddClaim(claim);
await Task.CompletedTask;
}
};
})
.AddOpenIdConnect("test",o => {
o.SignInScheme = "Cookies";
o.ClientId = "id";
o.ClientSecret = "08";
o.Authority = "https://ex.com";
o.ResponseType = "code";
o.MetadataAddress = "https://ex.com/.well-known/openid-configuration";
o.SaveTokens = true;
o.GetClaimsFromUserInfoEndpoint = true;
});
services.AddAuthorization(options =>
{
options.AddPolicy("BasicAuthentication", new AuthorizationPolicyBuilder("BasicAuthentication").RequireAuthenticatedUser().Build());
});
services.AddControllersWithViews();
services.AddRazorPages();
}

You don't need to try to get the ID/Access-token manually, it is all handled by the AddOpenIdConnect middleware. When the request comes back with the authorization code, then AddOpenIdConnect will automatically get the tokens for you.
In your code I would change to the following because the typical case is that you want to have the OpenIDConnect handler to handle the challenge part.
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})

Related

Keep getting 401 on authorization with SignalR

I have been trying to create a sample Test app with SignalR but I have been extremely unsuccessful with authentication.
I am on .NET 6, and my Program.cs code looks like this.
Program.cs
using HubTestApp.Hubs;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddSignalR();
builder.Services
.AddAuthentication(options =>
{
// Identity made Cookie authentication the default.
// However, we want JWT Bearer Auth to be the default.
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Authority = "https://login.microsoftonline.com/{TenantId}/";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateLifetime = true,
ValidateAudience = false,
ValidateIssuerSigningKey = false,
};
// We have to hook the OnMessageReceived event in order to
// allow the JWT authentication handler to read the access
// token from the query string when a WebSocket or
// Server-Sent Events request comes in.
// Sending the access token in the query string is required due to
// a limitation in Browser APIs. We restrict it to only calls to the
// SignalR hub in this code.
// See https://docs.microsoft.com/aspnet/core/signalr/security#access-token-logging
// for more information about security considerations when using
// the query string to transmit the access token.
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/Test")))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapHub<SimpleHub>("/Test");
app.Run();
My hub code is pretty simple:
namespace HubTestApp.Hubs
{
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
internal class SimpleHub : Hub<ISimpleClient>
{
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task EchoMessage(string message) => await Clients.All.ReceiveMessage(message);
}
}
And this is my client code:
namespace HubTestClient
{
using Microsoft.AspNetCore.SignalR.Client;
public class MockClient
{
private const string Token = "Bearer <JWT from AAD>";
private readonly HubConnection hubConnection;
public MockClient()
{
// Notice here I have tried to pass the token in various ways, all to no avail.
this.hubConnection = new HubConnectionBuilder()
.WithUrl($"http://localhost:5110/Test?access_token={MockClient.Token}", options =>
{
options.AccessTokenProvider = () => Task.FromResult(MockClient.Token);
options.Headers.Add("Authorization", MockClient.Token);
})
.Build();
this.hubConnection.On<string>("ReceiveMessage", (message) =>
{
Console.WriteLine($"Received message: '{message}'");
});
}
public async Task StartClient()
{
await hubConnection.StartAsync();
Random rng = new Random();
while (true)
{
string messageToEcho = $"Sending random number '{rng.Next()}'";
Console.WriteLine(messageToEcho);
try
{
await hubConnection.InvokeAsync("EchoMessage", messageToEcho);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
// Delay 5 seconds between hitting the hub.
await Task.Delay(5000);
}
}
}
}
I continuously get the message "Failed to invoke 'X' because user is unauthorized." I have made sure the token I got is valid. So, I'm bashing my head over this, completely confused where I am going wrong. Thank you in advance!

ASP.NET framework authentication with jwt and openidconnect

I have an ASP.NET MVC application (.Net Framework 4.7.2) using openidconnect authentication with okta configured. When the user is not authorised, the app redirects the user to okta for login, and it works fine for web browser.
We have a requirement to allow mobile app to render certain pages within app via web view and they will be passing request header authorization with access token.
After a bit Google search, I found that I can add jwt and openidconnect authentication so will check request header for authorization header if exist we will use jwt else openidconnect.
I tried with .NET Core 2.2 and it works fine, but I'm not sure how can I implement something similar in .net framework.
.NET Core code snippet
services.AddAuthentication("DefaultPolicy")
.AddJwtBearer(options => {
options.Authority = Configuration["Okta:Issuer"];
options.Audience = "auth";
})
.AddCookie()
.AddOpenIdConnect(options => {
options.ClientId = Configuration["Okta:ClientId"];
options.ClientSecret = Configuration["Okta:ClientSecret"];
options.Authority = Configuration["Okta:Issuer"];
options.CallbackPath = "/authorization-code/callback";
options.ResponseType = "code";
options.SaveTokens = true;
options.UseTokenLifetime = false;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.TokenValidationParameters = new TokenValidationParameters {
NameClaimType = "name"
};
})
.AddPolicyScheme("DefaultPolicy", "Authorization Bearer or OIDC", o => {
o.ForwardAuthenticate = "AuthenticateSignInPolicy";
o.ForwardSignIn = "AuthenticateSignInPolicy";
o.ForwardChallenge = "ChallengePolicy";
})
.AddPolicyScheme("AuthenticateSignInPolicy", "Authorization Bearer or OIDC", options => {
options.ForwardDefaultSelector = context => {
var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
if (authHeader?.StartsWith("Bearer ") == true)
{
return JwtBearerDefaults.AuthenticationScheme;
}
return CookieAuthenticationDefaults.AuthenticationScheme;
};
})
.AddPolicyScheme("ChallengePolicy", "Authorization Bearer or OIDC", options => {
options.ForwardDefaultSelector = context => {
var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
if (authHeader?.StartsWith("Bearer ") == true)
{
return JwtBearerDefaults.AuthenticationScheme;
}
return OpenIdConnectDefaults.AuthenticationScheme;
};
});
I will suppose here, that you are following the quick start provided by OKTA:
https://developer.okta.com/quickstart/#/okta-sign-in-page/dotnet/aspnet4.
Inside their guide, they tell you to add a Startup class. You need to replace their "app.UseOktaMVC" by "app.AddJwtBearerAuthentication".
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.AddJwtBearerAuthentication(new OktaWebApiOptions()
{
OktaDomain = Constants.GetIssuer,
AuthorizationServerId = string.Empty,
Audience = Constants.GetAudience,
});
}
}
The extension is provided by OKTA. If you want to see how to register it all by yourself, their source code is available on github.
https://github.com/okta/okta-aspnet/blob/master/Okta.AspNet/OktaMiddlewareExtensions.cs

JWT + SignalR on ASP Core 3 resulting in 401 Unauthorized

If I use http calls outside of signalr, such as with postman or httpclient, I am able to have my token validated successfully on the server. It's when I try to connect through my signalr hub that the token is not passing authorization.
Bearer was not authenticated. Failure message: No SecurityTokenValidator available for token: Bearer MyTokenFooBar
My service setup is:
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting();
services.AddControllers();
services.AddHealthChecks();
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(builder => { builder.ConnectionString = _configuration.GetConnectionString("DefaultConnection"); }));
services.AddIdentity<ApplicationUser, IdentityRole>(setup =>
{
// foo
}).AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = _configuration["Jwt:Issuer"],
ValidAudience = _configuration["Jwt:Audience"],
ValidateIssuer = false,
ValidateAudience = false,
ValidateIssuerSigningKey = false,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"])),
ValidateLifetime = false
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var path = context.HttpContext.Request.Path;
if (!path.StartsWithSegments("/chat")) return Task.CompletedTask;
var accessToken = context.Request.Headers[HeaderNames.Authorization];
if (!string.IsNullOrWhiteSpace(accessToken) && context.Scheme.Name == JwtBearerDefaults.AuthenticationScheme)
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
services.AddAuthorization();
services.AddSignalR(options => { options.EnableDetailedErrors = true; });
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(options =>
{
options.MapHealthChecks("/health");
options.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
});
app.UseSignalR(options => { options.MapHub<ChatHub>("/chat"); });
}
I use a basic http auth header for the initial connection, which will sign the user into identity and generate a jwt token as a response for use in future calls.
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Login()
{
var (headerUserName, headerPassword) = GetAuthLoginInformation(HttpContext);
var signInResult = await _signInManager.PasswordSignInAsync(headerUserName, headerPassword, false, false);
if (!signInResult.Succeeded)
{
return Unauthorized();
}
var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("SuperTopSecretKeyThatYouDoNotGiveOutEver!"));
var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);
var jwt = new JwtSecurityToken(signingCredentials: signingCredentials);
var handler = new JwtSecurityTokenHandler();
var token = handler.WriteToken(jwt);
return new OkObjectResult(token);
}
And my client (a console application) is setup to cache this token and use it in future signalr calls as such:
Get the token:
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(encoding.GetBytes($"{userName}:{password}")));
var response = await _client.SendAsync(request); // this goes to the login action posted above
_token = await response.Content.ReadAsStringAsync();
...
_hubConnection = new HubConnectionBuilder()
.WithUrl(new Uri(_baseAddress, "chat"),
options => { options.AccessTokenProvider = () => Task.FromResult(_token); }) // send the cached token back with every request
.Build();
// here is where the error occurs. 401 unauthorized comes back from this call.
await _hubConnection.StartAsync();
Resolved.
The issue was that I was overriding the OnMessageReceived handler of the JwtBearerHandler and then having it read the incoming token myself... but the token I was passing it included the prefix Bearer, which when parsed by the above handler did not match the known token for the existing user.
Simply removing my override of OnMessageReceived and letting AspNetCore's deafult implementation of the JwtBearerHandler do its job allowed the token parsing to work correctly.

SignalR using IdentityServer bearer won't receive any JWTBearerEvents from Hub

We have an api (.net core 2.2) which use IdentityServerAuthenticationDefaults.AuthenticationScheme for all the controllers which works fine.
We now decide to add SignalR Hub for a conference service.
The hub is working fine only if we remove the authorize attribute [Authorize(AuthenticationSchemes = IdentityServerAuthenticationDefaults.AuthenticationScheme)]
We did try to handle the token in the query using the following both methods (TokenRetriever or JwrBearerEvents) :
services.AddAuthentication()
.AddIdentityServerAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Authority = AuthURL;
options.SupportedTokens = SupportedTokens.Jwt;
options.RequireHttpsMetadata = HttpsSetting;
options.ApiName = APIs.API_Commerce;
options.TokenRetriever = new Func<HttpRequest, string>(req =>
{
var fromHeader = TokenRetrieval.FromAuthorizationHeader();
var fromQuery = TokenRetrieval.FromQueryString();
return fromHeader(req) ?? fromQuery(req);
});
options.JwtBearerEvents.OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/hubs/")))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
};
});
For some reason theses only fire when we call controllers but ignore all invoked methods from the client.
Note that we have an AuthServer which provide the tokens and an API.
We are using angular 7 with aspnet/signalr module for the client side.
I found the problem...
app.UseAuthentication() was added in Configure
Add default scheme to authentication and remove onmessagereceive ->
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityServerAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = IdentityServerAuthenticationDefaults.AuthenticationScheme;
})
.AddIdentityServerAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Authority = AuthURL;
options.SupportedTokens = SupportedTokens.Jwt;
options.RequireHttpsMetadata = HttpsSetting;
options.ApiName = APIs.API_Commerce;
options.TokenRetriever = new Func<HttpRequest, string>(req =>
{
var fromHeader = TokenRetrieval.FromAuthorizationHeader();
var fromQuery = TokenRetrieval.FromQueryString();
return fromHeader(req) ?? fromQuery(req);
});
});
Just to mention with .net core 2.2 u must specified an origin (withOrigins) and cannot use Any..

Custom Claims are not being accessed in client with identityserver 4 .Net core 2.0

I have following in my client startup.cs.
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; // cookie middle setup above
options.Authority = AuthSetting["Authority"]; // Auth Server
options.RequireHttpsMetadata = false; // only for development
options.ClientId = AuthSetting["ClientId"]; // client setup in Auth Server
options.ClientSecret = AuthSetting["ClientSecret"];
options.ResponseType = "code id_token"; // means Hybrid flow (id + access token)
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
//options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email", ClaimValueTypes.Email);
//options.ClaimActions.Clear(); //https://stackoverflow.com/a/47896180/9263418
//options.ClaimActions.MapUniqueJsonKey("Aes", "Aes");
//options.ClaimActions.MapUniqueJsonKey("foo", "foo");
//options.ClaimActions.MapJsonKey("Aes", "Aes"); //https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers/issues/210
});
Following is my Identityserver's startup.cs
services.AddIdentityServer(options =>
{
options.Events.RaiseSuccessEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
})
.AddInMemoryClients(Clients.Get())
.AddInMemoryIdentityResources(Resources.GetIdentityResources())
.AddInMemoryApiResources(Resources.GetApiResources())
.AddDeveloperSigningCredential()
.AddExtensionGrantValidator<Extensions.ExtensionGrantValidator>()
.AddExtensionGrantValidator<Extensions.NoSubjectExtensionGrantValidator>()
.AddJwtBearerClientAuthentication()
.AddAppAuthRedirectUriValidator()
.AddClientConfigurationValidator<DefaultClientConfigurationValidator>()
.AddProfileService<ProfileService>();
Following is my ProfileService.cs file.
public class ProfileService : IProfileService
{
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
// Processing
var claims = new List<Claim>
{
new Claim("Email", "someone2gmail.com"),
};
context.IssuedClaims.AddRange(claims);
return Task.FromResult(0);
}
public Task IsActiveAsync(IsActiveContext context)
{
// Processing
context.IsActive = true;
return Task.FromResult(0);
}
}
I am not able to access Mail claim in client application.
Checked many references.
But none of them are working for me. Any guess that what might be missing?
Using Identityserver4 with .Net core 2.
Never mind. I got it resolved by trying following option in client configuration of server. Will read it entirely. But for now it works as it seems to be including claims in token.
AlwaysIncludeUserClaimsInIdToken = true
The default scopes for OpenIDConnectOptions are "openid" and "profile".
You will have to additionally request the "email" scope when configuring your options.

Resources