How to use the same authorization pipeline for WebAPI with SignalR - authentication

I am adding signalr functionality to a website that only logged in users will have access to for communicating with each other. I also would like to use the same pipeline to allow azure worker roles to communicate with a user indicating job statuses for long running queue-based processes, running on their behalf. I found an example here at this url that outlines how to use the OAuth Bearer tokens with signalr: http://blog.marcinbudny.com/search?q=Authentication+signalr+OAuth+#.V5KBaY52xTM. The problem is that his example seems to turn off the OAuthBearerTokens which I am using in my code for web api authentication and authorization. Here is the code from the article:
//app.UseCookieAuthentication(new CookieAuthenticationOptions());
//app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
//app.UseOAuthBearerTokens(OAuthOptions);
app.UseOAuthAuthorizationServer(OAuthOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
{
Provider = new ApplicationOAuthBearerAuthenticationProvider(),
});
and this is my code utilizing the technique in this article:
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
PublicClientId = "self";
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId, UserFactory),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(2),
AllowInsecureHttp = false
};
app.Map("/signalr", map =>
{
map.UseCors(CorsOptions.AllowAll);
map.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()
{
Provider = new QueryStringOAuthBearerAuthenticationProvider()
});
var hubConfiguration = new HubConfiguration
{
Resolver = GlobalHost.DependencyResolver,
};
map.RunSignalR(hubConfiguration);
});
How can I allow signalr to access the user's information in the same way I would for web api when there are times I will need worker role processes to pass information to clients as well as client-side code does?

Related

Wanting to have seperate policies for SignIn and SignUp while using MSAL.net - both receiving webhook for AuthorizationCodeReceived

I have a .net mvc project based on b2c-webapi-dotnet ([https://github.com/Azure-Samples/active-directory-b2c-dotnet-webapp-and-webapi][1]) where I can register users or allow them to sign in via b2c (both operations having the same SignUpSignIn policy). This works as I expect.
I'm now trying to split this single policy into two, so having a SignUp policy and an additional SignIn policy
As part of this, both policies need to hit the AuthorizationCodeReceived hook so that I can pull out the b2c oid guid. This is used to find additional information on a user stored in a separate database.
What I am finding is that OnAuthorizationCodeReceived is only called when using one of my policies, its the default policy that I set into the MetadataAddress property when setting up the authorization.
In the code below, whichever policy I set for Settings.B2C_DefaultPolicyId (either SignIn or SignUp) is the one that has OnAuthorizationCodeReceived called.
Can anyone tell me if there is a way to have both policies be able to call OnAuthorizationCodeReceived ?
I appreciate any help you can give.
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
// ASP.NET web host compatible cookie manager
CookieManager = new SystemWebChunkingCookieManager()
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
// Generate the metadata address using the tenant and policy information
MetadataAddress = String.Format(WellKnownMetadata, Common.Settings.AppSettings.B2C_Tenant, Settings.B2C_DefaultPolicyId),
// These are standard OpenID Connect parameters, with values pulled from web.config
ClientId = Settings.AppSettings.B2C_ClientID,
RedirectUri = RedirectUri,
PostLogoutRedirectUri = PostLogoutRedirectUri,
// Specify the callbacks for each type of notifications
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = OnRedirectToIdentityProvider,
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = OnAuthenticationFailed,
},
// Specify the claim type that specifies the Name property.
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
ValidateIssuer = false
},
// Specify the scope by appending all of the scopes requested into one string (separated by a blank space)
Scope = $"openid profile offline_access {Settings.AppSettings.B2C_ReadTasksScope} {Settings.AppSettings.B2C_WriteTasksScope}",
// ASP.NET web host compatible cookie manager
CookieManager = new SystemWebCookieManager()
}
);
}
I've figured it out with the help of a colleague
From the example code I had, one of the callbacks had some code that needed to be updated
private Task OnRedirectToIdentityProvider(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
var policy = notification.OwinContext.Get<string>("Policy");
if (!string.IsNullOrEmpty(policy) && !policy.Equals(Globals.DefaultPolicy))
{
notification.ProtocolMessage.Scope = OpenIdConnectScope.OpenId;
notification.ProtocolMessage.ResponseType = OpenIdConnectResponseType.IdToken;
notification.ProtocolMessage.IssuerAddress = notification.ProtocolMessage.IssuerAddress.ToLower().Replace(Globals.DefaultPolicy.ToLower(), policy.ToLower());
}
return Task.FromResult(0);
}
The ResponseType needs to be changed from IdToken to CodeIdToken. Doing this means the non default Policy also hits the callback AuthorizationCodeReceived
One side affect of doing this is that after OnAuthorizationCodeReceived is hit, the callback for AuthenticationFailed is also hit.
To handle this, I've just put a conditional statement in the OnAuthenticationFailed method that returns to the root view (notification.Response.Redirect("/"))

How to Retrieve Claims from Idp-Initiated Login Using Sustainsys Saml2?

I am trying to add support for SAML authentication to an ASP.NET Core MVC application with ASP.NET Core Identity (not IdentityServer). The flow "works" when testing with StubIdp - the SAMLResponse is POSTed to /Saml2/Acs and I'm redirected to the app with an Identity.External cookie, but my ClaimsPrincipal is empty and unauthenticated. Even if I use the NameID of a user who already exists in the database, the claims are completely empty.
I also see the following in the console log:
Sustainsys.Saml2.AspNetCore2.Saml2Handler: Information: Successfully processed SAML response Microsoft.IdentityModel.Tokens.Saml2.Saml2Id and authenticated JohnDoe
I installed the Sustainsys.Saml2.AspNetCore2 package, and added the service configuration to startup.cs as follows:
services.AddAuthentication()
.AddSaml2(async options =>
{
var azureServiceTokenProvider = new AzureServiceTokenProvider();
var keyVaultClient = new KeyVaultClient(
new KeyVaultClient.AuthenticationCallback(
azureServiceTokenProvider.KeyVaultTokenCallback));
var certificateSecret = await keyVaultClient.GetSecretAsync($"https://{Configuration["KeyVaultName"]}.vault.azure.net/", Configuration["ServiceProviderCertName"]);
var privateKeyBytes = Convert.FromBase64String(certificateSecret.Value);
options.SPOptions.EntityId = new EntityId(Configuration["BaseUrl"] + "/Saml2");
options.SPOptions.ReturnUrl = new Uri(Configuration["BaseUrl"]);
IdentityProvider idp = new IdentityProvider(
new EntityId("https://stubidp.sustainsys.com/Metadata"), options.SPOptions)
{
LoadMetadata = true,
MetadataLocation = "https://stubidp.sustainsys.com/Metadata",
AllowUnsolicitedAuthnResponse = true
};
options.IdentityProviders.Add(idp);
options.SPOptions.ServiceCertificates.Add(new X509Certificate2(privateKeyBytes));
});
Configuration["BaseUrl"] is the base URL of my app, in this case a localhost port.
I'm obviously missing something, but I can't figure out what. Do I need to somehow explicitly connect/map the Saml2 service to ASP.NET Core Identity?
Was able to resolve this based on comments in this GitHub issue.
My comment explaining how I was able to implement the workaround: https://github.com/Sustainsys/Saml2/issues/1030#issuecomment-616842796

Trouble getting ClaimsPrincipal populated when using EasyAuth to authenticate against AAD on Azure App Service in a Asp.Net Core web app

We have a web app built on Asp.Net core. It doesn't contain any authentication middleware configured in it.
We are hosting on Azure App Service and using the Authentication/Authorization option (EasyAuth) to authenticate against Azure AD.
The authentication works well - we get the requisite headers inserted and we can see the authenticated identity at /.auth/me. But the HttpContext.User property doesn't get populated.
Is this a compatibility issue for Asp.Net core? Or am I doing something wrong?
I've created a custom middleware that populates the User property until this gets solved by the Azure Team.
It reads the headers from the App Service Authentication and create a a user that will be recognized by the [Authorize] and has a claim on name.
// Azure app service will send the x-ms-client-principal-id when authenticated
app.Use(async (context, next) =>
{
// Create a user on current thread from provided header
if (context.Request.Headers.ContainsKey("X-MS-CLIENT-PRINCIPAL-ID"))
{
// Read headers from Azure
var azureAppServicePrincipalIdHeader = context.Request.Headers["X-MS-CLIENT-PRINCIPAL-ID"][0];
var azureAppServicePrincipalNameHeader = context.Request.Headers["X-MS-CLIENT-PRINCIPAL-NAME"][0];
// Create claims id
var claims = new Claim[] {
new System.Security.Claims.Claim("http://schemas.microsoft.com/identity/claims/objectidentifier", azureAppServicePrincipalIdHeader),
new System.Security.Claims.Claim("name", azureAppServicePrincipalNameHeader)
};
// Set user in current context as claims principal
var identity = new GenericIdentity(azureAppServicePrincipalIdHeader);
identity.AddClaims(claims);
// Set current thread user to identity
context.User = new GenericPrincipal(identity, null);
};
await next.Invoke();
});
Yes, this is a compatibility issue. ASP.NET Core does not support flowing identity info from an IIS module (like Easy Auth) to the app code, unfortunately. This means HttpContext.User and similar code won't work like it does with regular ASP.NET.
The workaround for now is to invoke your web app's /.auth/me endpoint from your server code to get the user claims. You can then cache this data as appropriate using the x-ms-client-principal-id request header value as the cache key. The /.auth/me call will need to be properly authenticated in the same way that calls to your web app need to be authenticated (auth cookie or request header token).
I wrote a small basic middleware to do this. It will create an identity based off of the .auth/me endpoint. The identity is created in the authentication pipeline so that [authorize] attributes and policies work with the identity.
You can find it here:
https://github.com/lpunderscore/azureappservice-authentication-middleware
or on nuget:
https://www.nuget.org/packages/AzureAppserviceAuthenticationMiddleware/
Once added, just add this line to your startup:
app.UseAzureAppServiceAuthentication();
The following code decrypts the AAD token from the Azure App Service HTTP header and populates HttpContext.User with the claims. It's rough as you'd want to cache the configuration rather than look it up on every request:
OpenIdConnectConfigurationRetriever r = new OpenIdConnectConfigurationRetriever();
ConfigurationManager<OpenIdConnectConfiguration> configManager = new ConfigurationManager<OpenIdConnectConfiguration>(options.Endpoint, r);
OpenIdConnectConfiguration config = await configManager.GetConfigurationAsync();
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKeys = config.SigningKeys.ToList(),
ValidateIssuer = true,
ValidIssuer = config.Issuer,
ValidateAudience = true,
ValidAudience = options.Audience,
ValidateLifetime = true,
ClockSkew = new TimeSpan(0, 0, 10)
};
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
ClaimsPrincipal principal = null;
SecurityToken validToken = null;
string token = context.Request.Headers["X-MS-TOKEN-AAD-ID-TOKEN"];
if (!String.IsNullOrWhiteSpace(token))
{
principal = handler.ValidateToken(token, tokenValidationParameters, out validToken);
var validJwt = validToken as JwtSecurityToken;
if (validJwt == null) { throw new ArgumentException("Invalid JWT"); }
if (principal != null)
{
context.User.AddIdentities(principal.Identities);
}
}
It only works for Azure AD. To support other ID providers (Facebook, Twitter, etc) you'd have to detect the relevant headers and figure out how to parse each provider's token. However, it should just be variations on the above theme.
You can give this library a try. I faced a similar problem and created this to simplify the use.
https://github.com/dasiths/NEasyAuthMiddleware
Azure App Service Authentication (EasyAuth) middleware for ASP.NET
CORE with fully customizable components with support for local
debugging
It hydrates the HttpContext.User by registering a custom authentication handler. To make things easier when running locally, it even has the ability to use a json file to load mocked claims.

Signing in to an application with ws-federation from front-end application

I have two applications, one web-api application (y.x.com) and a front-end application (z.x.com). To authenticate the user who visits z.x.com I use ws-federation or microsoft live login following the web api template code provided by visual studio 2015. If I talk directly to the web api application (y.x.com) from my browser, postman, fiddler or anything similar the authentication works fine but if I try to sign in from the front-end application I get error: invalid_request (status 400).
Now I wonder if it should be possible to sign in from application z.x.com by calling y.x.com/Account/ExternalLogin?provider=Federation&response_type=token&client_id=self&redirect_uri=http://y.x.com.
My startup.auth in y.x.com looks like this
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AuthorizeEndpointPath = new PathString("/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
// In production mode set AllowInsecureHttp = false
AllowInsecureHttp = true
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
var wsOptions = new WsFederationAuthenticationOptions
{
MetadataAddress = "https://login.microsoftonline.com/afd2d5a6-bdb1-43f8-a42b-83ec49f1f22d/federationmetadata/2007-06/federationmetadata.xml",
Wtrealm = "http://y.x.com/",
Notifications = new WsFederationAuthenticationNotifications()
};
app.UseWsFederationAuthentication(wsOptions);
I can provide more code but I'm mostly interested in if should work at all.
Thanks.
This is possible. After som digging and help it turns out that in the web-api template there is a method named ValidateClientRedirectUri in the class ApplicationOAuthProvider. If I change that method to
public override Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
context.Validated();
return Task.FromResult<object>(null);
}
and then from my front end application I can now have any return url I want, making it possible to sign in from the front-end application via the web-api application to an external source.

OpenId Connect middleware not setting auth cookie in an WebForms app

I'm trying to integrate OpenId Connect into long-time existing webforms application. I was able to migrate the app to use OWIN and I'm using OpenIdConnectAuthenticationMiddleware to authenticate against my IdP provider. All goes fine until the point where I need to construct new identity obtained from IdP and set the cookie - which part I think is not happening.
Important parts of my Startup.Configure method:
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/login.aspx"),
CookieManager = new SystemWebCookieManager() //custom cookie manager
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://[development_domain]/core",
ClientId = "VDWeb",
ResponseType = "code id_token token",
Scope = "openid profile",
UseTokenLifetime = true,
SignInAsAuthenticationType = "Cookies",
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = async n =>
{
var userInfo = await EndpointAndTokenHelper.CallUserInfoEndpoint(n.ProtocolMessage.AccessToken);
//now store Preferred name :
var prefNameClaim = new Claim(
Thinktecture.IdentityModel.Client.JwtClaimTypes.PreferredUserName,
userInfo.Value<string>("preferred_username"));
var myIdentity = new ClaimsIdentity(
n.AuthenticationTicket.Identity.AuthenticationType,
Thinktecture.IdentityModel.Client.JwtClaimTypes.PreferredUserName,
Thinktecture.IdentityModel.Client.JwtClaimTypes.Role);
myIdentity.AddClaim(prefNameClaim);
//add unique_user_key claim
var subjectClaim = n.AuthenticationTicket.Identity.FindFirst(Thinktecture.IdentityModel.Client.JwtClaimTypes.Subject);
myIdentity.AddClaim(new Claim("unique_user_key", subjectClaim.Value));
myIdentity.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
var ticket = new AuthenticationTicket(myIdentity, n.AuthenticationTicket.Properties);
var currentUtc = new SystemClock().UtcNow;
ticket.Properties.IssuedUtc = currentUtc;
ticket.Properties.ExpiresUtc = currentUtc.Add(TimeSpan.FromHours(12));
n.AuthenticationTicket = ticket;
},
}
});
I can confirm AuthentocationTicket is populated properly but auth cookie is NOT set. I do know about this issue https://katanaproject.codeplex.com/workitem/197 and I have tried all workarounds offered for this issue but none helped. Interestingly enough, when I try to drop my own cookie inside of SecurityTokenValidated event - n.Response.Cookies.Append("Test", "Test");, I can see the cookie is set properly.
One of the workarounds suggest implementing your own CookieManager. What makes me curious is that when I put a breakpoint into cookie setter in this custom manager, it is not hit, i.e. middleware seems not even trying to set the cookie. So the main question I have - at what point exactly the middleware will try to set the cookie? Is it when I set my AuthenticationTicket?
Edit 1: adding more information. I tried to compare with another web app, this time MVC, that I configured to use the same IdP and that works as expected. Startup code for both apps is the same. When debugging thru SecurityTokenValidated event, I can see that MVC app (working) has created System.Security.Principal.WindowsPrincipal identity while webforms app (non-working) created System.Security.Principal.GenericIdentity identity.
I have also added this little snipped
app.UseStageMarker(PipelineStage.Authenticate);
app.Use((context, next) =>
{
var identity = context.Request.User.Identity;
return next.Invoke();
});
just to see what identity get populated on this pipeline stage. For MVC app (working) I see the identity I added by setting AuthenticationTicket, for webforms app I still see non-authenticated GenericIdentity.
OK, this is embarrassing - the problem was in CookieAuthenticationOptions, apparently AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie is NOT the same as AuthenticationType = "Cookies". Once set this later way, it is working fine.
Can you use the default cookie manager and see if that results in a cookie being set?