Authenticate a .NET Core 2.1 SignalR console client to a web app that uses "Identity as UI" - asp.net-core

Using .NET Core 2.1 & VS2017 preview 2 I created a simple web server with "Identity as UI" as explained here and then added a SignalR chat following this.
In particular I have:
app.UseAuthentication();
app.UseSignalR((options) => {
options.MapHub<MyHub>("/hubs/myhub");
});
..
[Authorize]
public class MyHub : Hub
..
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:5000",
"sslPort": 0
I start the debugger which brings the browser, register a user and log in, then go to http://localhost:5000/SignalRtest (my razor page that uses signalr.js) and verify the chat works fine.
I now try to create a .NET Core console app chat client:
class Program
{
public static async Task SetupSignalRHubAsync()
{
var hubConnection = new HubConnectionBuilder()
.WithUrl("http://localhost:5000/hubs/myhub")
.Build();
await hubConnection.StartAsync();
await hubConnection.SendAsync("Send", "consoleapp");
}
public static void Main(string[] args)
{
SetupSignalRHubAsync().Wait();
}
}
My issue is I don't know how to authenticate this client ?
EDIT:
(from https://github.com/aspnet/SignalR/issues/2455)
"The reason it works in the browser is because the browser has a
Cookie from when you logged in, so it sends it automatically when
SignalR makes it's requests. To do something similar in the .NET
client you'll have to call a REST API on your server that can set the
cookie, scoop the cookie up from HttpClient and then provide it in the
call to .WithUrl, like so:
var loginCookie = /* get the cookie */
var hubConnection = new HubConnectionBuilder()
.WithUrl("http://localhost:5000/hubs/myhub", options => {
options.Cookies.Add(loginCookie);
})
.Build();
I now put a bounty on this question, hoping to get a solution showing how to authenticate the .NET Core 2.1 SignalR console client with a .NET Core 2.1 web app SignalR server that uses "Identity as UI". I need to get the cookie from the server and then add it to SignalR HubConnectionBuilder (which now have a WithCookie method).
Note that I am not looking for a third-party solutions like IdentityServer
Thanks!

I did ended up getting this to work like this:
On the server I scaffolded Login and then in Login.cshtml.cs added
[AllowAnonymous]
[IgnoreAntiforgeryToken(Order = 1001)]
public class LoginModel : PageModel
That is I do not require the anti forgery token on login (which doesn't make sense anyway)
The client code is then like this:
HttpClientHandler handler = new HttpClientHandler();
CookieContainer cookies = new CookieContainer();
handler.CookieContainer = cookies;
HttpClient client = new HttpClient(handler);
var uri = new Uri("http://localhost:5000/Identity/Account/Login");
string jsonInString = "Input.Email=myemail&Input.Password=mypassword&Input.RememberMe=false";
HttpResponseMessage response = await client.PostAsync(uri, new StringContent(jsonInString, System.Text.Encoding.UTF8, "application/x-www-form-urlencoded"));
var responseCookies = cookies.GetCookies(uri);
var authCookie = responseCookies[".AspNetCore.Identity.Application"];
var hubConnection = new HubConnectionBuilder()
.WithUrl("http://localhost:5000/hubs/myhub", options =>
{
options.Cookies.Add(authCookie);
})
.Build();
await hubConnection.StartAsync();
await hubConnection.SendAsync("Send", "hello!");
(of course password will be elsewhere in deployment)

Related

Migrating to Openiddict 4.0.0 breaks HotChocolate GraphQL authorization for client credentials flow if token encryption is enabled

I've an ASP.NET Core project which hosts both an identity server using Openiddict and a resource server using HotChocolate GraphQL package.
Client-credentials flow is enabled in the system. The token is encrypted using RSA algorithm.
Till now, I had Openiddict v3.1.1 and everything used to work flawlessly. Recently, I've migrated to Openiddict v4.0.0. Following this, the authorization has stopped working. If I disable token encyption then authorization works as expected. On enabling token encyption, I saw in debugging, that claims are not being passed at all. I cannot switch off token encyption, as it is a business and security requirement. The Openiddict migration guidelines doesn't mention anything about any change related to encryption keys. I need help to make this work as Openiddict v3.1.1 is no longer supported for bug fixes.
The OpenIddict setup in ASP.NET Core pipeline:
public static void AddOpenIddict(this IServiceCollection services, IConfiguration configuration)
{
var openIddictOptions = configuration.GetSection("OpenIddict").Get<OpenIddictOptions>();
var encryptionKeyData = openIddictOptions.EncryptionKey.RSA;
var signingKeyData = openIddictOptions.SigningKey.RSA;
var encryptionKey = RSA.Create();
var signingKey = RSA.Create();
encryptionKey.ImportFromEncryptedPem(encryptionKeyData.ToCharArray(),
openIddictOptions.EncryptionKey.Passphrase.ToCharArray());
signingKey.ImportFromEncryptedPem(signingKeyData.ToCharArray(),
openIddictOptions.SigningKey.Passphrase.ToCharArray());
encryptionKey.ImportFromEncryptedPem(encryptionKeyData.ToCharArray(),
openIddictOptions.EncryptionKey.Passphrase.ToCharArray());
signingKey.ImportFromEncryptedPem(signingKeyData.ToCharArray(),
openIddictOptions.SigningKey.Passphrase.ToCharArray());
var sk = new RsaSecurityKey(signingKey);
var ek = new RsaSecurityKey(encryptionKey);
services.AddOpenIddict()
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<AuthDbContext>()
.ReplaceDefaultEntities<Guid>();
})
.AddServer(options =>
{
// https://documentation.openiddict.com/guides/migration/30-to-40.html#update-your-endpoint-uris
options.SetCryptographyEndpointUris("oauth2/.well-known/jwks");
options.SetConfigurationEndpointUris("oauth2/.well-known/openid-configuration");
options.SetTokenEndpointUris("oauth2/connect/token");
options.AllowClientCredentialsFlow();
options.SetUserinfoEndpointUris("oauth2/connect/userinfo");
options.SetIntrospectionEndpointUris("oauth2/connect/introspection");
options.AddSigningKey(sk);
options.AddEncryptionKey(ek);
//options.DisableAccessTokenEncryption(); // If this line is not commented, things work as expected
options.UseAspNetCore(o =>
{
// NOTE: disabled because by default OpenIddict accepts request from HTTPS endpoints only
o.DisableTransportSecurityRequirement();
o.EnableTokenEndpointPassthrough();
});
})
.AddValidation(options =>
{
options.UseLocalServer();
options.UseAspNetCore();
});
}
Authorization controller token get action:
[HttpPost("~/oauth2/connect/token")]
[Produces("application/json")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest();
if (request.IsClientCredentialsGrantType())
{
// Note: the client credentials are automatically validated by OpenIddict:
// if client_id or client_secret are invalid, this action won't be invoked.
var application = await _applicationManager.FindByClientIdAsync(request.ClientId);
if (application == null)
{
throw new InvalidOperationException("The application details cannot be found in the database.");
}
// Create a new ClaimsIdentity containing the claims that
// will be used to create an id_token, a token or a code.
var identity = new ClaimsIdentity(
authenticationType: TokenValidationParameters.DefaultAuthenticationType,
nameType: OpenIddictConstants.Claims.Name,
roleType: OpenIddictConstants.Claims.Role);
var clientId = await _applicationManager.GetClientIdAsync(application);
var organizationId = await _applicationManager.GetDisplayNameAsync(application);
// https://documentation.openiddict.com/guides/migration/30-to-40.html#remove-calls-to-addclaims-that-specify-a-list-of-destinations
identity.SetClaim(type: OpenIddictConstants.Claims.Subject, value: organizationId)
.SetClaim(type: OpenIddictConstants.Claims.ClientId, value: clientId)
.SetClaim(type: "organization_id", value: organizationId);
identity.SetDestinations(static claim => claim.Type switch
{
_ => new[] { OpenIddictConstants.Destinations.AccessToken, OpenIddictConstants.Destinations.IdentityToken }
});
return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
throw new NotImplementedException("The specified grant type is not implemented.");
}
Resource controller (where Authorization is not working):
public class Query
{
private readonly IMapper _mapper;
public Query(IMapper mapper)
{
_mapper = mapper;
}
[HotChocolate.AspNetCore.Authorization.Authorize]
public async Task<Organization> GetMe(ClaimsPrincipal claimsPrincipal,
[Service] IDbContextFactory<DbContext> dbContextFactory,
CancellationToken ct)
{
var organizationId = Ulid.Parse(claimsPrincipal.FindFirstValue(ClaimTypes.NameIdentifier));
... // further code removed for brevity
}
}
}
GraphQL setup in ASP.NET Core pipeline:
public static void AddGraphQL(this IServiceCollection services, IWebHostEnvironment webHostEnvironment)
{
services.AddGraphQLServer()
.AddAuthorization()
.AddQueryType<Query>()
}
Packages with versions:
OpenIddict (4.0.0)
OpenIddict.AspNetCore (4.0.0)
OpenIddict.EntityFrameworkCore (4.0.0)
HotChocolate.AspNetCore (12.13.2)
HotChocolate.AspNetCore.Authorization (12.13.2)
HotChocolate.Diagnostics (12.13.2)
I think you can look at the logs to figure out exactly what's going on:
Logging in .NET Core and ASP.NET Core.
Not sure if the Openiddict 4.0 release changed encryption key related configuration, but if you can't get encryption keys to work perhaps you can remove the OpenIddict validation part and configure the JWT bearer handler instance to use the appropriate encryption key , a configuration similar to this.
Of course, you can also open an issue in GitHub to ask #Kévin Chalet.

.Net Core / Identity Server - Is it possible to AllowAnonymous but only from my client?

I have a REST API and an IdentityServer set up. I would like to be able to display items in my client from the API without having to sign in. However, I would also like to protect my API from external clients that don't belong to me. Is it possible to AllowAnonymous but only from my client?
[HttpGet]
[AllowAnonymous]
public List<Item> GetItems()
{
return new List<Item> { "item1", "item2" };
}
Edit w/ Solution
As mentioned by Tore Nestenius, I changed the grant types from Code to CodeAndClientCredentials and added the Authorize attribute to my controller so that only my client can access it.
Controller:
[HttpGet]
[Authorize]
public List<Item> GetItems()
{
return new List<Item> { "item1", "item2" };
Identity Server 4 Config File:
public static IEnumerable<Client> Clients =>
new Client[]
{
new Client
{
ClientId = "postman-api",
AllowedGrantTypes = GrantTypes.CodeAndClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
}
};
}
CORS only works for requests from browsers, if a non browser application makes a request, then CORS will not be involved.
if you use [AllowAnonymous], then any client can access that API endpoint. Either you create separate client for the general things, perhaps using the Client Credentials flow, so that the client can authenticate, get its own token without any user involved.
Turns out this is handled by CORS.
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddDefaultPolicy(
builder => builder
.WithOrigins("yourURL")
.AllowAnyMethod());
})
}

How to automatically login Okta user with active login session to .NET 4.5 Web Forms app

I've got a .NET 4.5 Web Forms app with Okta authentication on top. The authentication setup seems to be working fine; I can login and logout and get my Okta user info/claims from the context variable.
What I'd like to do is detect on page load whether a user already has an active Okta session in their browser and then log them into the application. Or if they don't have a session do nothing and stay on the application page.
Making a challenge call to the authentication manager
HttpContext.Current.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties { RedirectUri = "/Login.aspx" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
almost does what I want. If the user has an active session it'll do some redirects and log them in. But if they're not logged in they get sent to, and left on, the Okta login page. Which is not what I want.
I thought I would be able to access some cookies that Okta sets when a user logs in via an Okta page, but when checking through the browser debugger and checking Request.Cookies they don't seem to be available at that stage. And the context doesn't have access to the user info either.
edit: Also, if it helps, this is what my Startup.cs looks like
using System;
using System.Threading.Tasks;
using Microsoft.Owin;
using Owin;
using IdentityModel.Client;
using Microsoft.AspNet.Identity;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using System.Collections.Generic;
using System.Configuration;
using System.Security.Claims;
[assembly: OwinStartup(typeof(WebApplication.Startup))]
namespace WebApplication
{
public class Startup
{
// These values are stored in Web.config. Make sure you update them!
private readonly string _clientId = ConfigurationManager.AppSettings["okta:ClientId"];
private readonly string _redirectUri = ConfigurationManager.AppSettings["okta:RedirectUri"];
private readonly string _authority = ConfigurationManager.AppSettings["okta:OrgUri"];
private readonly string _clientSecret = ConfigurationManager.AppSettings["okta:ClientSecret"];
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
public void ConfigureAuth(IAppBuilder app)
{
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = _clientId,
ClientSecret = _clientSecret,
Authority = _authority,
RedirectUri = _redirectUri,
ResponseType = OpenIdConnectResponseType.CodeIdToken,
Scope = "openid profile email offline_access",
TokenValidationParameters = new TokenValidationParameters { NameClaimType = "name" },
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
// Exchange code for access and ID tokens
var tokenClient = new TokenClient($"{_authority}/v1/token", _clientId, _clientSecret);
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, _redirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
var userInfoClient = new UserInfoClient($"{_authority}/v1/userinfo");
var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);
var claims = new List<Claim>(userInfoResponse.Claims)
{
new Claim("code", n.Code),
new Claim("id_token", tokenResponse.IdentityToken),
new Claim("refresh_token", tokenResponse.RefreshToken),
new Claim("access_token", tokenResponse.AccessToken)
};
n.AuthenticationTicket.Identity.AddClaims(claims);
},
},
});
}
}
}

Re-challenge authenticated users in ASP.NET Core

I'm running into some issues with the authentication pipeline in ASP.NET Core. My scenario is that I want to issue a challenge to a user who is already authenticated using OpenID Connect and Azure AD. There are multiple scenarios where you'd want to do that, for example when requesting additional scopes in a AAD v2 endpoint scenario.
This works like a charm in ASP.NET MVC, but in ASP.NET Core MVC the user is being redirected to the Access Denied-page as configured in the cookie authentication middleware. (When the user is not logged in, issuing a challenge works as expected.)
After a couple of hours searching the web and trying different parameters for my middleware options, I'm beginning to suspect that either I'm missing something obvious, or this behavior is by design and I need to solve my requirement some other way. Anyone any ideas on this?
EDIT: the relevant parts of my Startup.cs look like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthentication(
SharedOptions => SharedOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// <snip...>
app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme });
var options = new OpenIdConnectOptions
{
AuthenticationScheme = OpenIdConnectDefaults.AuthenticationScheme,
ClientId = ClientId,
Authority = Authority,
CallbackPath = Configuration["Authentication:AzureAd:CallbackPath"],
ResponseType = OpenIdConnectResponseType.CodeIdToken,
PostLogoutRedirectUri = "https://localhost:44374/",
TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = false
}
};
options.Scope.Add("email");
options.Scope.Add("offline_access");
app.UseOpenIdConnectAuthentication(options);
}
And the Action looks like this:
public void RefreshSession()
{
HttpContext.Authentication.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/" });
}
I found a hint and the solution here: https://github.com/aspnet/Security/issues/912.
ChallengeBehavior.Unauthorized is the "key".
This post gives the current (november 2016 - ASPNet 1.0.1) workaround: https://joonasw.net/view/azure-ad-b2c-with-aspnet-core
You'll need a new ActionResult to be able to call the AuthauticationManager.ChallengeAsync with the ChallengeBehavior.Unauthorized behavior.
Once the issue https://github.com/aspnet/Mvc/issues/5187 will be sucessfully closed, this should be integrated.
I tested it and it worked perfectly well (my goal was simply to extend Google scopes on a per user basis).
Try to sign out:
public void RefreshSession()
{
HttpContext.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
HttpContext.Authentication.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
HttpContext.Authentication.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/" });
}

OWIN Authentication in MVC 5 Loops while User.IsAuthenticated = false

I'm trying to implement authentication and authorization in an MVC 5.1 app. The authentication takes place via Facebook that is custom implemented. (I can post that code if needed.) Once FB authenticates and sends back the code and the Authenticate method of the auth service is called to sign the user into the application. There is no auth code in the application itself (thus not using Identity or other membership services).
public async Task<ActionResult> Connect(string code)
{
if (code == null)
{
return RedirectToAction("Index", "Home");
}
else
{
// get access token
var accessToken = await nApplication.FacebookClient.AccessTokenAsync(code);
// get user info from facebook
var meResult = await nApplication.FacebookClient.MeResultAsync(accessToken);
nApplication.NRepository.SaveChanges();
nAuthorization.Authenticate(member);
return RedirectToAction("Index");
}
}
nAuthorization.Authenticate(member); creates a list of claims and executes OWIN SignIn,
claims.Add(new Claim(ClaimTypes.Name, member.Name));
claims.Add(new Claim(ClaimTypes.Role, "Member"));
var claimIdentity = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie);
owinContext.Authentication.SignIn(new AuthenticationProperties { IsPersistent = true }, claimIdentity);
I'm using the Authorize attribute from Mvc namespace. But at this point /Profile/Authenticate/ which is my Owin LoginPath get's called again and again to redirect the user to FB and return to the Connect method above.
[Authorize(Roles = "Member")]
public async Task<ActionResult> Index(int? id)
I've checked the User property in the controller and it is not authenticated. I could set that to a new ClaimsPrincipal but I'd like the auth code to be independent of the HttpContext. And it doesn't seem to be right solution.
My Startup class contains:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
LoginPath = new PathString("/Profile/Authenticate/"),
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
CookieSecure = CookieSecureOption.Always,
ReturnUrlParameter = "next"
});
Maybe I am missing something completely fundamental? Any pointers would help, I've looked through articles such as the following but to no avail:
http://brockallen.com/2013/10/24/a-primer-on-owin-cookie-authentication-middleware-for-the-asp-net-developer/
http://www.khalidabuhakmeh.com/asp-net-mvc-5-authentication-breakdown-part-deux
I think this will solve your problem...it worked for me:
Add an empty method in your global.asax.cs file:
protected void Session_Start()
{
}
for some reason, the asp.net session cookie does not get set at the proper time without this. The Thinktecture devs think this might be happening if your webapp uses http and your identity provider uses https but I have not verified that yet.