ADFS with existing ASP.Net MVC App - asp.net-mvc

I've been looking around, trying to find an example of how to add ADFS authentication to an existing ASP.Net MVC application. I found lots of example of how to do it using the wizard when you create a new app.
I could create a new app and copy the code and config over, but this seams like a strange approach.
Does anyone know of a good guide or resource?

We found this blog entry on Cloud Identity to be really helpful to get started with something similar. We are using Web API so it's not exactly the same.
You will need to add this to your Startup.Auth.cs file:
app.UseActiveDirectoryFederationServicesBearerAuthentication(
new ActiveDirectoryFederationServicesBearerAuthenticationOptions
{
Audience = ConfigurationManager.AppSettings["ida:Audience"],
MetadataEndpoint = ConfigurationManager.AppSettings["ida:MetadataEndpoint"]
});
In your web.config you will need keys to point to those entries:
<add key="ida:AdfsMetadataEndpoint" value="https://adfs.yourdomain.com/federationmetadata/2007-06/federationmetadata.xml" />
<add key="ida:Audience" value="https://yourmvc.yourdomain.com" />
Note that what version of ADFS you are using makes a big difference. We found that while trying to get tokens to work with version 3.0 of ADFS they are somewhat broken at the moment. On premises ADFS will also work much differently than Azure.
We needed to customize the claims for our implementation and this post helped immensely. Startup.Auth.cs will look similar to this:
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Audience = ConfigurationManager.AppSettings["ida:Audience"],
Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
Provider = new OAuthBearerAuthenticationProvider()
{
OnValidateIdentity = async context =>
{
context.Ticket.Identity.AddClaim(
new Claim(http://mycustomclaims/hairlenght,
RetrieveHairLenght(userID),
ClaimValueTypes.Double,
"LOCAL AUTHORITY");));
}
}
});

Related

implement Microsoft Authentication (owin) on an existing WebForms web app

I'm trying to implement Microsoft Authentication (owin) on an existing WebForms web application. I can't use the official guides from Microsoft, since they are made for modern solutions with MVC.
Any tips on where to start? I can't find any guides.
Please follow the document which helps you in old .NET forms to use OWIN Forms middleware and to use this middleware for Azure AD authentication please follow Docs.
Install the package for Microsoft.Owin.Security.Cookies
Right click the App_Start folder on your project Add -> New Item - > "OWIN Startup Class"
then create the following folder
public void Configuration(IAppBuilder app)
{
CookieAuthenticationOptions opt = new CookieAuthenticationOptions();
opt.AuthenticationType = CookieAuthenticationDefaults.AuthenticationType;// "Identity.Application";
opt.CookieName = ".SSO";
opt.CookieDomain = "localhost";
opt.SlidingExpiration = true;
ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("192.168.1.96:6379");
IDataProtector proc = DataProtectionProvider.Create(new DirectoryInfo(#"C:\test\core"), buildAction =>
buildAction.SetApplicationName("MyApp").SetDefaultKeyLifetime(TimeSpan.FromDays(9000)).ProtectKeysWithDpapi().PersistKeysToStackExchangeRedis(redis)).CreateProtector("Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", "Cookies", "v2");
DataProtectorShim shim = new DataProtectorShim(proc);
opt.TicketDataFormat = new AspNetTicketDataFormat(shim);
app.UseCookieAuthentication(opt);
}

Sustainsys SAML2 Sample for ASP.NET Core WebAPI without Identity

Does anyone have a working sample for Sustainsys Saml2 library for ASP.NET Core WebAPI only project (no Mvc) and what's more important without ASP Identity? The sample provided on github strongly relies on MVC and SignInManager which I do not need nor want to use.
I added Saml2 authentication and at first it worked fine with my IdP (I also checked the StubIdP provided by Sustainsys) for first few steps so:
IdP metadata get properly loaded
My API properly redirects to sign-in page
Sign-in page redirects to /Saml2/Acs page, and I see in the logs that it parses the result successfully
However I don't know how to move forward from there and extract user login and additional claims (my IdP provided also an e-mail, and it is included in SAML response which I confirmed in the logs).
Following some samples found on the web and modyfing a little bit the MVC Sample from GitHub I did the following:
In Startup.cs:
...
.AddSaml2(Saml2Defaults.Scheme,
options =>
{
options.SPOptions.EntityId = new EntityId("...");
options.SPOptions.ServiceCertificates.Add(...));
options.SPOptions.Logger = new SerilogSaml2Adapter();
options.SPOptions.ReturnUrl = new Uri(Culture.Invariant($"https://localhost:44364/Account/Callback?returnUrl=%2F"));
var idp =
new IdentityProvider(new EntityId("..."), options.SPOptions)
{
LoadMetadata = true,
AllowUnsolicitedAuthnResponse = true, // At first /Saml2/Acs page throwed an exception that response was unsolicited so I set it to true
MetadataLocation = "...",
SingleSignOnServiceUrl = new Uri("...") // I need to set it explicitly because my IdP returns different url in the metadata
};
options.IdentityProviders.Add(idp);
});
In AccountContoller.cs (I tried to follow a somewhat similar situation described at how to implement google login in .net core without an entityframework provider):
[Route("[controller]")]
[ApiController]
public class AccountController : ControllerBase
{
private readonly ILog _log;
public AccountController(ILog log)
{
_log = log;
}
[HttpGet("Login")]
[AllowAnonymous]
public IActionResult Login(string returnUrl)
{
return new ChallengeResult(
Saml2Defaults.Scheme,
new AuthenticationProperties
{
// It looks like this parameter is ignored, so I set ReturnUrl in Startup.cs
RedirectUri = Url.Action(nameof(LoginCallback), new { returnUrl })
});
}
[HttpGet("Callback")]
[AllowAnonymous]
public async Task<IActionResult> LoginCallback(string returnUrl)
{
var authenticateResult = await HttpContext.AuthenticateAsync(Constants.Auth.Schema.External);
_log.Information("Authenticate result: {#authenticateResult}", authenticateResult);
// I get false here and no information on claims etc.
if (!authenticateResult.Succeeded)
{
return Unauthorized();
}
// HttpContext.User does not contain any data either
// code below is not executed
var claimsIdentity = new ClaimsIdentity(Constants.Auth.Schema.Application);
claimsIdentity.AddClaim(authenticateResult.Principal.FindFirst(ClaimTypes.NameIdentifier));
_log.Information("Logged in user with following claims: {#Claims}", authenticateResult.Principal.Claims);
await HttpContext.SignInAsync(Constants.Auth.Schema.Application, new ClaimsPrincipal(claimsIdentity));
return LocalRedirect(returnUrl);
}
TLDR: Configuration for SAML in my ASP.NET Core WebApi project looks fine, and I get success response with proper claims which I checked in the logs. I do not know how to extract this data (either return url is wrong or my callback method should work differently). Also, it is puzzling why successfuly redirect from SSO Sign-In page is treated as "unsolicited", maybe this is the problem?
Thanks for any assistance
For anyone who still needs assistance on this issue, I pushed a full working example to github which uses a .Net Core WebAPI for backend and an Angular client using the WebAPI. you can find the example from here:
https://github.com/hmacat/Saml2WebAPIAndAngularSpaExample
As it turned out, the various errors I've been getting were due to my solution being hosted inside docker container. This caused a little malfunction in internal aspnet keychain. More details can be found here (docker is mentioned almost at the end of the article):
https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview?tabs=aspnetcore2x&view=aspnetcore-2.2
Long story short, for the code to be working I had to add only these lines:
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("/some/volume/outside/docker")); // it needs to be outside container, even better if it's in redis or other common resource
It fixed everything, which includes:
Sign-in action to external cookie
Unsolicited SSO calls
Exceptions with data protection key chain
So it was very difficult to find, since exceptions thrown by the code didn't point out what's going on (and the unsolicited SSO calls made me think that the SSO provider was wrongly configured). It was only when I disassembled the Saml2 package and tried various code pieces one by one I finally encoutered proper exception (about the key chain) which in turned led me to an article about aspnet data protection.
I provide this answer so that maybe it will help someone, and I added docker tag for proper audience.

Function to request user claims and token from Identity Server?

What is the best way to retrieve user claims and tokens after logging into identity server?
I have a .Net Core MVC web app. I redirect to an identity server, log in, then redirect back to my web app.
From my app I would then like to request user claims and token from identity server.
Is there a method call to do this?
I have gone through the tutorials and searched around and found libraries (oidc-client-js, oidc.usermanager for javascript that handles this. OidcClient for native c# apps) that handle this behavior, but not for c# web app - so I realize maybe what I'm asking for is not the convention but I'd still appreciate any insight on this.
Edit:
To elaborate, in the c# web app tutorials, specifically tutorial #3, I fail to see how claims and token information become retrievable from from figure 1 to figure 2.
Furthermore, I'm trying to call and handle these objects in c#, not html.
Figure 1
Figure 2
Thank you
In controller , you can retire claims like
var claims = User.Claims;
Get the access token/id token/refresh token :
var accessToken = HttpContext.GetTokenAsync("access_token").Result;
var idToken = HttpContext.GetTokenAsync("id_token").Result;
var refreshToken = HttpContext.GetTokenAsync("refresh_token").Result;
To achieve that , you need register your OIDC middleware inside ConfigureServices as follows (making sure to set SaveTokens to true) :
.AddOpenIdConnect(options => {
// Set all your OIDC options...
// and then set SaveTokens to save tokens to the AuthenticationProperties
options.SaveTokens = true;
});
In mvc razor page , you can get the information :
#using Microsoft.AspNetCore.Authentication
#{
ViewData["Title"] = "Home Page";
}
<dl>
#foreach (var claim in User.Claims)
{
<dt>#claim.Type</dt>
<dd>#claim.Value</dd>
}
</dl>
<dt>access token</dt>
<dd>#await ViewContext.HttpContext.GetTokenAsync("access_token")</dd>
<dt>ID token</dt>
<dd>#await ViewContext.HttpContext.GetTokenAsync("id_token")</dd>
<dt>refresh token</dt>
<dd>#await ViewContext.HttpContext.GetTokenAsync("refresh_token")</dd>

.NET Core 2.0 dual authentication (Mixing MVC and API)

I am a back-end developer who typically develops APIs and and administration panels all under the same project. I primary use Laravel/PHP, however I recently started delving into .NET Core.
In PHP (Laravel), I could tie my API endpoints and webpages to the same controller actions. For example, An API consumer should be able to create a blog post using the API endpoint. Also, the administrator should be able to create a blog post using the web UI, which should follow the same validation logic, etc.
Here is some pseudocode for the create action.
class BlogPostController
{
//Create a new blog post
function create(request)
{
authorizeAction(); //Make sure the current user is authorized to create a blog post
validateFields(request); //Make sure the posted data is valid for a blog post
//Create the blog post
blogPost = new BlogPost(request);
if(request.isAPI)
return json(blogPost); //Return the new blog post as a json string
return view(blogPost); //Return an HTML representation of the blog post (in administration panel)
}
}
In Laravel, since all routes can have different middleware, I could define two routes to the above action eg POST /api/blogpost and POST /blogpost, each with different authentication schemes. (api would authorize via a JWT token, and the web url would auth via a cookie).
I'm having trouble doing something similar in .NET Core since different authentication schemes cannot be applied via separate middleware as of Version 2 (as far as I know).
My solution to this problem would be adding two authentication schemes like so:
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); // => remove default claims
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddCookie(cfg=>cfg.SlidingExpiration = true)
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = Configuration["JwtIssuer"],
ValidAudience = Configuration["JwtIssuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtKey"])),
ClockSkew = TimeSpan.Zero
};
});
And then on my controller use:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme + "," + CookieAuthenticationDefaults.AuthenticationScheme)]
This would effectively challenge a JWT token and a cookie and use whichever one it found. This seems a bit messy to me (and a Microsoft employee would agree)
Am I approaching this whole problem wrong? To me it makes sense to keep all of the logic in one controller and just return json or HTML depending on the type of request. I could have a separate controller for the API endpoints and the webpages, but it seems like a lot of duplication of code. The other option would be to ditch the idea completely and just build an API/Single Page Application that consumes the API.
Is this simply bad design, or is there any elegant way of using different authentication schemes based on the route?

A simple ASP .NET MVC API controller using roles

I wrote a web application using ASP .NET MVC and authorization system by default. I configured IdentityRole and input through external providers. Using the current database I have created my data context. Now I want to write a Xamarin.Android app and connect to my database, I want a simple API. But the feature that you want to access this API was only available to user with a certain role. The API is really very simple and therefore do not want to add to the draft WCF or WebAPI project. How to do it best?
First, you don't need a separate project to use Web Api; you can use both MVC and Web Api in the same project. For one off endpoints for things like in-site AJAX requests, just creating MVC actions that return JSON or XML would be fine, but if you're talking about a true API, even if it's fairly simplistic, I'd say go Web Api.
You'd protect your Web Api actions much the same as you would your MVC actions, using the [Authorize] attribute. If you need to restrict by role, you just pass a role(s) to that. However, the big difference here, especially if you're serving a mobile app, is that you'll need pass the authorization along with the request. That's generally accomplished using the Authorization header along with a bearer token. Basically, you would need to set up an endpoint that signs a user in and returns a token. Then, each subsequent request that needs authorization includes that token in the header.
I want to finish and to fully answer this question and close this topic. I've been searching for how to add the ability for a mobile client to connect to an existing site on ASP.NET MVC. In my search, I came across a great article Justin Hyland on March 2, 2014
In principle, everything in this article is well and clearly written, but I want to make a tiny contribution for clarity.
Under Setup WebAPIConfig stated that the need
added in the following code to the WebApiConfig Register method
But if we consider the case ASP.NET MVC we don't have such file. It's all very simple, you just need such a file to create the folder App_Start. The contents of the file can be left exactly as it is in the article.
To get rid of the bugs which will inevitably appear we need to install two nuget package: Microsoft.AspNet.WebApi and Microsoft.AspNet.WebApi.Owin.
Excellent! Now we can turn to the method to obtain the token and then adding the token to the query we can get the needed data closed by the attribute [Authorize].
A small remark. If You need to access a method which is closed for a specific role that to the Authenticate method from the article should add a few lines of code. Immediately after the line:
identity.AddClaim(new Claim(ClaimTypes.Name, user));
add the line:
identity.AddClaim(new Claim(ClaimTypes.Role, role));
where role you can get the following, for example:
var userIdentity = UserManager.FindAsync(user, password).Result;
var role = RoleManager.FindById(userIdentity.Roles.First().RoleId).Name;
User and password you have to send a request.
I also want to give an example of code which will send request and receive response. To not have to look for and immediately start coding.
async Task<string> GetToken(string userName, string password)
{
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>( "user", userName ),
new KeyValuePair<string, string> ( "password", password )
}
);
using (var client = new HttpClient())
{
HttpResponseMessage response = await client.PostAsync(APP_PATH + "/Authenticate", content);
var result = await response.Content.ReadAsStringAsync();
return result;
}
}
async Task<string> GetUserInfo(string token)
{
using (var client = CreateClient(token))
{
var response = await client.GetAsync(APP_PATH + "/ValidateToken");
return await response.Content.ReadAsStringAsync();
}
}
HttpClient CreateClient(string accessToken = "")
{
var client = new HttpClient();
if (!string.IsNullOrWhiteSpace(accessToken))
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
}
return client;
}
All have only to call the appropriate methods in the correct order. I hope that is useful to someone.
P.S.
If You create a new project in Visual Studio to get this functionality you just need to tick:

Resources