I've been trying to figure this out for a while now, reading a lot of blogs, MSDN documentation, sample code and other stackoverflow questions and have yet to get this to work.
Here is my scenario:
I am using Windows Azure to host two web roles. One is my MVC4 web API, the other is my MVC4 web app which uses the web API. I also have a number of client applications using .NET that will access the web API.
So my main components are:
Web API
Web App
.NET Client
I want to use forms authentication that is 'hosted' in the Web App. I am using the built in simplemembership authentication mechanism and it works great. I can create and log in to accounts in the Web App.
Now I also want to use these same accounts to authenticate the Web API, both from the Web App and any .NET client apps.
I've read numerous ways to do this, the simplest appearing to be using Basic Authentication on the Web API. Currently I am working with this code as it appears to solve my exact problem: Mixing Forms Authentication, Basic Authentication, and SimpleMembership
I can't get this to work. I log in successfully to my Web App (127.0.0.1:81) and when I try to call a Web API that requires authentication (127.0.0.1:8081/api/values for example) the call fails with a 401 (Unauthorized) response. In stepping through the code, WebSecurity.IsAuthenticated returns false. WebSecurity.Initialized returns true.
I've implemented this code and am trying to call my Web API from my Web App (after logging in) with the following code:
using ( var handler = new HttpClientHandler() )
{
var cookie = FormsAuthentication.GetAuthCookie( User.Identity.Name, false );
handler.CookieContainer.Add( new Cookie( cookie.Name, cookie.Value, cookie.Path, cookie.Domain ) );
using ( var client = new HttpClient() )
{
//client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
// "Basic",
// Convert.ToBase64String( System.Text.ASCIIEncoding.ASCII.GetBytes(
// string.Format( "{0}:{1}", User.Identity.Name, "123456" ) ) ) );
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Cookie",
Convert.ToBase64String( System.Text.ASCIIEncoding.ASCII.GetBytes( User.Identity.Name ) ) );
string response = await client.GetStringAsync( "http://127.0.0.1:8080/api/values" );
ViewBag.Values = response;
}
}
As you can see, I've tried both using the cookie as well as the username/password. Obviously I want to use the cookie, but at this point if anything works it will be a good step!
My ValuesController in my Web API is properly decorated:
// GET api/values
[BasicAuthorize]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
In my Global.asax.cs in my Web API, I am initializing SimpleMembership:
// initialize our SimpleMembership connection
try
{
WebSecurity.InitializeDatabaseConnection( "AzureConnection", "User", "Id", "Email", autoCreateTables: false );
}
catch ( Exception ex )
{
throw new InvalidOperationException( "The ASP.NET Simple Membership database could not be initialized. For more information, please see http://go.microsoft.com/fwlink/?LinkId=256588", ex );
}
This succeeds and WebSecurity later says that it is initialized so I guess this part is all working properly.
My config files have matching authentication settings as required per MSDN.
Here is the API config:
<authentication mode="Forms">
<forms protection="All" path="/" domain="127.0.0.1" enableCrossAppRedirects="true" timeout="2880" />
</authentication>
<machineKey decryption="AES" decryptionKey="***" validation="SHA1" validationKey="***" />
Here is the Web App config:
<authentication mode="Forms">
<forms loginUrl="~/Account/Login" protection="All" path="/" domain="127.0.0.1" enableCrossAppRedirects="true" timeout="2880" />
</authentication>
<machineKey decryption="AES" decryptionKey="***" validation="SHA1" validationKey="***" />
Note, I am trying this locally (hence the 127.0.0.1 domain), but referencing a database hosted on Azure.
I haven't got to trying any of this from a .NET client application since I can't even get it working between web roles. For the client app, ideally I would make a web call, passing in username/password, retrieve the cookie, and then use the cookie for further web API requests.
I'd like to get what I have working as it seems pretty simple and meets my requirements.
I have not yet tried other solutions such as Thinktecture as it has way more features than I need and it doesn't seem necessary.
What am I missing?
Well, this is embarrassing. My main problem was a simple code error. Here is the correct code. Tell me you can spot the difference from the code in my question.
using ( var handler = new HttpClientHandler() )
{
var cookie = FormsAuthentication.GetAuthCookie( User.Identity.Name, false );
handler.CookieContainer.Add( new Cookie( cookie.Name, cookie.Value, cookie.Path, cookie.Domain ) );
using ( var client = new HttpClient( handler ) )
...
}
Once that was fixed, I started getting 403 Forbidden errors. So I tracked that down and made a small change to the BasicAuthorizeAttribute class to properly support the [BasicAuthorize] attribute when no role is specified.
Here is the modified code:
private bool isAuthorized( string username )
{
// if there are no roles, we're good!
if ( this.Roles == "" )
return true;
bool authorized = false;
var roles = (SimpleRoleProvider)System.Web.Security.Roles.Provider;
authorized = roles.IsUserInRole( username, this.Roles );
return authorized;
}
With that change basic authentication by passing in the forms cookie works!
Now to get non-web client apps working and then refactor the Web App as recommended.
I hope this helps someone in the future!
Related
I am trying to configure Windows authentication on a subroute only in my ASP.NET Core MVC app.
My problem is that when I add
services.AddAuthentication().AddNegotiate()
I get an error
The Negotiate Authentication handler cannot be used on a server that directly supports Windows Authentication.
which lead me to adding web.config as the docs explained:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<location path="." inheritInChildApplications="false">
<system.webServer>
<security>
<authentication>
<anonymousAuthentication enabled="false" />
<windowsAuthentication enabled="true" />
</authentication>
</security>
</system.webServer>
</location>
</configuration>
and the error goes away. However, now the Windows authentication is popping up on each request.
I tried changing the location path to .testendpoint but that then throws the original error at the base path.
So is it possible and how do I make such only /testendpoint will ask for Windows authentication and the remaining of the application will work with whatever other auth I configured in my ASP.NET Core app?
Another way using endpoint routing:
We have an application schema for the application that will be used all over the app called eavfw.
Using a custom endpoint here called login/ntlm with metadata new AuthorizeAttribute(NegotiateDefaults.AuthenticationScheme) its only allowed to be visited by a valid windows authenticated user.
Here we then create the user in our DB using its AD username.
endpoints.MapGet("/.auth/login/ntlm", async httpcontext =>
{
var loggger = httpcontext.RequestServices.GetRequiredService<ILogger<Startup>>();
var windowsAuth = await httpcontext.AuthenticateAsync(NegotiateDefaults.AuthenticationScheme);
if (!windowsAuth.Succeeded)
{
loggger.LogWarning("Not authenticated: Challening");
}
if (windowsAuth.Succeeded)
{
loggger.LogWarning("Authenticated");
var name = string.Join("\\", windowsAuth.Principal.Claims.FirstOrDefault(c => c.Type.EndsWith("name")).Value.Split("\\").Skip(1));
var context = httpcontext.RequestServices.GetRequiredService<DynamicContext>();
var users = context.Set<SystemUser>();
var user = await context.Set<SystemUser>().Where(c => c.PrincipalName == name).FirstOrDefaultAsync();
if (user == null)
{
user = new SystemUser
{
PrincipalName = name,
Name = name,
// Email = email,
};
await users.AddAsync(user);
await context.SaveChangesAsync();
}
var principal = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] {
new Claim(Claims.Subject,user.Id.ToString())
}, "ntlm"))
{
};
await httpcontext.SignInAsync("ntlm",
principal, new AuthenticationProperties(
new Dictionary<string, string>
{
["schema"] = "ntlm"
}));
httpcontext.Response.Redirect("/account/login/callback");
}
}).WithMetadata(new AuthorizeAttribute(NegotiateDefaults.AuthenticationScheme));
using a auxility authentication cookie, we can now make it such that specific areas of our app that requires windows authentication, it can simply rely on Authorize("ntlm") as it automatically forward the authenticate call to check if already signin, and it as part of the signin call in the endpoint above actually sign in eavfw.external before it redirects to the general account callback page that will do some final validation before signing in eavfw from the eavfw.external cookie
services.AddAuthentication().AddCookie("ntlm", o => {
o.LoginPath = "/.auth/login/ntlm";
o.ForwardSignIn = "eavfw.external";
o.ForwardAuthenticate = "eavfw";
});
So there are a few ways to extend and use the authentication system in auth core depending on how MVC framework heavy your application is.
Just thought I'd share this tidbit of information:
First off, just because you installed Windows Authentication with Server Manager, doesn't mean it's enabled in IIS. It's NOT enabled, by default.
You have to open IIS Manager, click on your server (NOT the website - the name of the server machine hosting IIS). Then click on Authentication - you will see "Windows Authentication" is disabled. Enable it. Now it will work.
Check this is correctly set first, before making other config changes. The default project for dotNet5 and dotNet6 will work w/o any modifications if IIS is correctly configured for Windows Authentication.
In order to have a certain page/action method secured via Windows authentation, specify the corresponding authentication scheme in the action methods Authorize attribute.
[Authorize(AuthenticationSchemes = IISServerDefaults.AuthenticationScheme)]
public IActionResult UsingWindowsAuthentication()
Make sure to have Windows authentication enabled on your website.
In order to use other authentication schemes, e.g. "Individual Accounts", anonymous authentication is also enabled.
The controllers and/or action methods that must not use Windows Authentication have the default scheme specified.
For example, for an ASP.NET Core MVC project that uses the out of the box "Individual Accounts" authentication type as default authentication method, that is Identity.Application.
[Authorize(AuthenticationSchemes = "Identity.Application")]
public IActionResult Index()
See the documentation about how to set up and configure multiple authentication schemes.
I have a working ServiceStack API that authenticates against a AzureAD tenant. We are trying to move this to start using Azure B2C. The application is build with c# and runs on net 5.0. I've managed to change the configuration to use the 'correct' config. I'm then using Postman to get my access token from my tenant suing the authorization code flow.
However, when i make a request to the api, the response is always a 401 status code.
Where in the servicestack code can I put a break point to see why this failure is happening? I have tried multiple places in our AppHostConfigurator.cs/AppHost.cs files, but the break points doesn't appear to display why a 401 is being sent back as a response. I'm sure it's something related to wrong claims/roles expected etc, maybe the Azure ADB2C application being setup incorrectly, but obviously i need to know exactly so that i can resolve.
I'm setting up the authentication like this:
private static void ConfigureAuthentication(IAppHost host)
{
var authProviders = new List<IAuthProvider> {new NetCoreIdentityAuthProvider(host.AppSettings)};
if (host.AppSettings.GetAllKeys().Contains("AzureAdB2C"))
{
var debugMode = host.AppSettings.Get(nameof(HostConfig.DebugMode), false);
var azureSettings = host.AppSettings.Get<AzureAdB2COptions>("AzureAdB2C");
var jwt = azureSettings.GetB2CJWTProviderReader(debugMode);
jwt.PopulateSessionFilter = (session, payload, request) =>
{
if (session.Email == null && payload.ContainsKey("upn") && payload["upn"].Contains("#"))
session.Email = payload["upn"];
if (session.UserName == null && payload.ContainsKey("unique_name"))
session.UserName = payload["unique_name"];
};
authProviders.Add(jwt);
}
var auth = new AuthFeature(() => new AuthUserSession(), authProviders.ToArray())
{
HtmlRedirect = "/account/signin",
HtmlLogoutRedirect = "/account/signout",
IncludeAssignRoleServices = false,
IncludeRegistrationService = false
};
// remove default service authentication services
auth.ServiceRoutes.Remove(typeof(AuthenticateService));
host.Plugins.Add(auth);
}
We are using swagger as well to call the API (which works as expected). This question is more about that requests that are submitted with a bearer token.
thanks
Please refer to this existing answer for examples of how to validate why a 3rd Party JWT Token is invalid with ServiceStack's JWT Auth Provider.
I setup an MVC project with Aspnet Core targeting Net461. Authentication is configured to use Adfs from a Windows Server 2016 system. I managed to get sign in working, however, when I click sign out I am given a page cannot be displayed error. Browsing back to the home url shows that the user is still logged in also. Any suggestions?
You might find this sample useful (even though it is for Azure ADFS, it works for local installs as well): https://github.com/Azure-Samples/active-directory-dotnet-webapp-openidconnect-aspnetcore
The logout action method like the following work well in my case:
[HttpGet]
public IActionResult SignOut()
{
var callbackUrl = Url.Action(nameof(SignedOut), "Account", values: null, protocol: Request.Scheme);
return SignOut(
new AuthenticationProperties { RedirectUri = callbackUrl },
CookieAuthenticationDefaults.AuthenticationScheme,
OpenIdConnectDefaults.AuthenticationScheme);
}
This will redirect you to the /Account/SignedOut after it completes and you need to register your /signout-callback-oidc endpoint for your client as well. This endpoint is used (by default) by the OIDC ASP.NET Core middleware.
Ok so i tried hosting the simplest oauth sample and the identity server both on iis, i have enable cors on the simplest oauth sample. So when i test the api using the javascript implicit client, on iis express it works flawlessly, it gets the token then when the token is sent the web api checks the token and authorizes the javascript client. the problem happens when i move the javascript imlicit client, the identity server, and the simple oath web api is hosted on iis, the javascript brings back the token correctly but when the token is sent to the web api it always return 401 unauthorized. So is there any configuration i have to add in order to run it on iis. i have made sure that anonymous authentication is the only enab;ed authentication mode. Any help or pointer is deeply appreciate.
I am trying to implement the samples given on iis. thanks for the help
I had the same issue. It was coming from my self signed certificate.
Try adding to your IdentityServerOptions
RequireSsl = false
and switch the WebApi Authority to use http.
Edit
Server Side Configuration
public void ConfigureIdentityServer(IAppBuilder app)
{
//Configure logging
LogProvider.SetCurrentLogProvider(new DiagnosticsTraceLogProvider());
//This is using a Factory Class that generates the client, user & scopes. Can be seen using the exmaples
var IdentityFactory = Factory.Configure("DefaultConnection");
app.Map("/identity", idsrvApp =>
{
idsrvApp.UseIdentityServer(new IdentityServerOptions
{
SiteName = "Security Proof of Concept",
SigningCertificate = LoadCertificate(),
Factory = IdentityFactory,
CorsPolicy = CorsPolicy.AllowAll,
RequireSsl = false
});
});
}
JavaScript
After receiving the token make sure it's inserted in the Authorization Header..
JQuery Example
$.ajax({
url: 'http://your.url',
type: GET,
beforeSend: function (xhr) {
xhr.withCredentials = true;
xhr.setRequestHeader("Authorization", " Bearer " + apiToken);
}
});
WebApi Resource
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
//Location of identity server make full url & port
Authority = "http://localhost/identity",
RequiredScopes = new[] { "WebApiResource" }
//Determines if the Api Pings the Identity Server for validation or will decrypt token by it's self
//ValidationMode = ValidationMode.Local
});
Best way to determine what is happening is enable logging.
I am playing with Google Drive Client API with MVC 4 web project. The code works great locally with IIS express. However, when I deploy the site to AppHarbor, the oAuth authentication hang. I tried both web client credentials and installed app client credentials. What do I need to do to get it working?
Here is the code snippet for Authentication:
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Message = "Modify this template to jump-start your ASP.NET MVC application.";
UserCredential credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
new ClientSecrets
{
ClientId = { Client_ID set in Google developer console},
ClientSecret = { Client secret in Google developer console},
},
new[] { DriveService.Scope.Drive },
"user",
CancellationToken.None).Result;
//Create the service.
var service = new DriveService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "Google Drive Reader",
});
//More code goes here
return View();
}
}
Update:
I figured this out and put an answer to this question in case others may what to know.
I figured this out.
First of all, the method I used in the question works only for standalone applications, does not work for MVC applications. MVC application should follow the method documented here:
https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth#web_applications
One important thing to notice is that web application client ID should be used, and the Redirect URL needs to be the URL to your AuthCallbackController.
Second, there is a problem in the Sample code: in HomeController
public async Task IndexAsync(CancellationToken cancellationToken)
Should be:
public async Task<ActionResult> IndexAsync(CancellationToken cancellationToken)
Third: make sure adding the following appSetting to web.config so that AppHarbor sends correct redirect url.
<appSettings>
<add key="aspnet:UseHostHeaderForRequestUrl" value="true" />
</appSettings>
After that, it worked for me both locally with IIS Express and on AppHarbor.