I am using a third party authentication procedure to authorize my pages in Nancy. I have tried to do it in MVC and it is successfull but I cannot reproduce the same results in Nancy.
Here is what I am doing:
MVC-Startup:
using Microsoft.Owin;
using Owin;
using Passport.Auth;
[assembly: OwinStartup(typeof(TestAuthorization.Startup))]
namespace TestAuthorization
{
public partial class Startup:StartupBase
{
public void Configuration(IAppBuilder app)
{
base.Configuration(app);
}
public override string IdentityServerUri
{
get { return "https://test.ThirdParty.URL/identity"; }
}
public override string RedirectUri
{
get { return "https://localhost:4443"; }
}
public override string ApplicationClientId
{
get { return "local.fox.company"; }
}
}
}
Nancy-Startup:
using Microsoft.Owin;
using Owin;
using Passport.Auth;
using Nancy.Owin;
[assembly: OwinStartupAttribute(typeof(AgreementManagementTool.Startup))]
namespace AgreementManagementTool
{
public class Startup: StartupBase
{
public void Configuration(IAppBuilder app)
{
app.UseNancy();
base.Configuration(app);
}
public override string IdentityServerUri
{
get { return "https://test.ThirdParty.URL/identity"; }
}
public override string RedirectUri
{
get { return "https://localhost:4443"; }
}
public override string ApplicationClientId
{
get { return "local.fox.company"; }
}
}
}
Now here is my program.cs for Nancy only:
class Program
{
static void Main(string[] args)
{
var uri = "https://+:4443"; //"https://localhost:4443";
Console.WriteLine("Starting Nancy on " + uri);
using (WebApp.Start<Startup>(uri))
{
Console.WriteLine("\n\nServer listening at {0}. Press enter to stop", uri);
Console.ReadLine();
return;
}
}
}
Now all I have to do is write [Authorize] on top of my Nancy module and it should work just like MVC.
MVC-Controller:
using System.Web.Mvc;
namespace TestAuthorization.Controllers
{
[Authorize]
public class HomeController : Controller
{
public ActionResult Index()
{
//this.RequiresMSOwinAuthentication();
return View();
}
}
}
Nancy-Module:
using Nancy;
using AgreementManagementTool.Views.Home.Model;
using System.Web.Mvc;
namespace AgreementManagementTool.Modules
{
[Authorize]
public class HomeModule : NancyModule
{
public HomeModule()
: base("/home")
{
Get["/"] = parameters =>
{
//this.RequiresMSOwinAuthentication(); // Not working
//this.RequiresAuthentication(); // Not working
HomeModel result = new HomeModel();
result.ResultImport = "I am testing AUthentication";
return View["index", result];
};
}
}
}
When I browse to the page after running the MVC application run successfully and authorization work successfull, but nancy doesnot show anything.
I have tried to use this.RequiresAuthentication(); but it throws an exception:
Nancy-Exception
Just to mention that I have no idea how the third party authentication process works, I just have to use it.
In MVC I have recieved the sample and it is working fine, why is it not working the same in nancy.
Nancy does not use [Authorize] attribute.
Have a look sample using Identity server for Nancy specific implementation.
If you are just using Nancy, try this to replace both your startups (using Owin.Security.Cookies):
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
namespace TestOwin
{
public class Startup
{
// OWIN Configuration goes here
public static void Configuration(IAppBuilder app)
{
var cookieOptions = new CookieAuthenticationOptions() { AuthenticationMode = AuthenticationMode.Active };
var nancyCfg = new NancyOptions();
nancyCfg.Bootstrapper = new NBootstrapper();
app.UseStaticFiles();
app.UseCookieAuthentication(cookieOptions);
app.UseNancy(nancyCfg);
}
}
}
And in your NancyBootstrapper (Here is where you can redirect users to a login page):
public class NBootstrapper : DefaultNancyBootstrapper
{
//Nancy Bootstrapper overrides go here
protected override void ConfigureApplicationContainer(TinyIoCContainer container)
{
// We don't call "base" here to prevent auto-discovery of
// types/dependencies
}
protected override void ConfigureRequestContainer(TinyIoCContainer container, NancyContext context)
{
base.ConfigureRequestContainer(container, context);
// Here we register our user mapper as a per-request singleton.
// As this is now per-request we could inject a request scoped
// database "context" or other request scoped services.
container.Register<IUserMapper, UserValidator>();
}
protected override void RequestStartup(TinyIoCContainer requestContainer, IPipelines pipelines, NancyContext context)
{
// At request startup we modify the request pipelines to
// include forms authentication - passing in our now request
// scoped user name mapper.
//
// The pipelines passed in here are specific to this request,
// so we can add/remove/update items in them as we please.
var formsAuthConfiguration =
new FormsAuthenticationConfiguration()
{
RedirectUrl = "~/login",
UserMapper = requestContainer.Resolve<IUserMapper>(),
};
FormsAuthentication.Enable(pipelines, formsAuthConfiguration);
}
}
And then you can use:
this.RequiresMSOwinAuthentication();
on your modules.
Hope this helps! My problem is trying to also get the same authentication to work inside Nancy and outside too, with SignalR...
Related
We are using ServiceStack for our .NET backend and I am trying to work on getting unit testing into the project. However there are some automated tools within ServiceStack that makes it a bit complicated to isolate the units so I could really use some advice. In the example below I would like to unit test a simple service that basically does the following:
Takes a request DTO
Passes the DTO to the repository
Gets back a domain model
If the model exists, it maps it to a responseDTO using Automapper and returns it as a part of an IHTTPResult
So the problem I have is that it seems like Automapper is automatically added to the ServiceStack application and in the application the mapper are registered by just calling:
AutoMapping.RegisterConverter().
So how could I inject this into the service to be able to do the unittest?
Example test:
using AutoMapper;
using FluentAssertions;
using NSubstitute;
namespace Api.Services.Tests.Unit;
public class OrderApiServiceTests
{
private readonly OrderApiService _sut;
private readonly IOrderApiRepository accountApiRepository = Substitute.For<IOrderApiRepository>();
public OrderApiServiceTests()
{
_sut = new OrderApiRepository(orderApiRepository);
var config = new MapperConfiguration(cfg => ApiDtoMapping.Register());
var mapper = config.CreateMapper();
}
[Fact]
public async Task Get_ShouldReturnAccount_WhenAccountExistsAsync()
{
// Arrange
var order = new Order
{
Name = "MyOrder",
Value = 1000,
};
var expectedResponse = new OrderApiDto
{
Name = "MyOrder",
Value = 1000,
};
orderApiRepository.GetAsync(Arg.Any<GetOrder>()).Returns(order);
// Act
var result = await _sut.Get(new GetOrder());
// Assert
result.StatusCode.Should().Be(System.Net.HttpStatusCode.OK);
result.Response.Should().BeEquivalentTo(expectedResponse);
}
}
Added a full example including all files:
// Program.cs
var builder = WebApplication.CreateBuilder(args);
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.UseServiceStack(new AppHost());
app.Run();
// Configure.AppHost.cs
using Funq;
using ssUnitTests.ServiceInterface;
[assembly: HostingStartup(typeof(ssUnitTests.AppHost))]
namespace ssUnitTests;
public class AppHost : AppHostBase, IHostingStartup
{
public void Configure(IWebHostBuilder builder) => builder
.ConfigureServices(services =>
{
});
public AppHost() : base("ssUnitTests", typeof(MyServices).Assembly) { }
public override void Configure(Container container)
{
container.RegisterAutoWiredAs<OrderRepository, IOrderRepository>().ReusedWithin(ReuseScope.None);
// Configure ServiceStack only IOC, Config & Plugins
SetConfig(new HostConfig
{
UseSameSiteCookies = true,
});
Mappings.RegisterConverters();
}
}
// Mappings.cs
using ssUnitTests.ServiceModel;
namespace ssUnitTests;
public static class Mappings
{
public static void RegisterConverters()
{
AutoMapping.RegisterConverter((Order from) =>
{
var to = from.ConvertTo<OrderDto>();
to.DtoProperty = from.BaseProperty + "Dto";
return to;
});
}
}
// IOrderRepository.cs
using ssUnitTests.ServiceModel;
namespace ssUnitTests.ServiceInterface;
public interface IOrderRepository
{
Order GetOrder();
}
// Order.cs
namespace ssUnitTests.ServiceModel;
public class Order
{
public string Name { get; set; }
public string BaseProperty { get; set; }
}
// OrderDto.cs
namespace ssUnitTests.ServiceModel;
public class OrderDto
{
public string Name { get; set; }
public string DtoProperty { get; set; }
}
// OrderRequest.cs
using ServiceStack;
namespace ssUnitTests.ServiceModel;
[Route("/order")]
public class OrderRequest : IReturn<OrderDto>
{
public int Id { get; set; }
}
// UnitTest.cs
using NSubstitute;
using NUnit.Framework;
using ssUnitTests.ServiceInterface;
using ssUnitTests.ServiceModel;
namespace ssUnitTests.Tests;
public class UnitTest
{
private readonly MyServices _sut;
private readonly IOrderRepository _repository = Substitute.For<IOrderRepository>();
public UnitTest()
{
_sut = new MyServices(_repository);
}
[Test]
public void Get_ShouldReturn_OrderDto()
{
var order = new Order
{
Name = "MyName",
BaseProperty = "MyBaseProperty"
};
_repository.GetOrder().Returns(order);
var response = (OrderDto)_sut.Any(new OrderRequest { Id = 1 });
Assert.That(response.Name.Equals(order.Name));
Assert.That(response.DtoProperty.Equals(order.BaseProperty + "Dto"));
}
}
ServiceStack.dll does not have any dependencies to any 3rd Party Libraries, e.g. it's built-in AutoMapping is a completely different stand-alone implementation to AutoMapper.
If you're using AutoMapper you can ignore ServiceStack's AutoMapping which is completely unrelated.
I implemented web api 2, authentication filter, based in this link https://learn.microsoft.com/en-us/aspnet/web-api/overview/security/authentication-filters.
The filter works, but I can't apply in on Controller? I can only apply it globally like this;
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Filters.Add(new MyAuthenticationFilter()); // Global level
MyAuthenticationFilter implementation
using Test1.Web.Areas.Api.Models;
using Test1.Web.Areas.Api.Provisioning;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.Filters;
public class MyAuthenticationFilter : IAuthenticationFilter
{
private static CustomerService = new CustomerService();
public bool AllowMultiple => true;
public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
// 1. Look for credentials in the request.
HttpRequestMessage request = context.Request;
AuthenticationHeaderValue authorization = request.Headers.Authorization;
// 2. If there are no credentials, do nothing.
if (authorization == null)
{
this.SetContextErrorResult(context);
return;
}
string apiKey = authorization.Scheme;
// 3. If there are credentials, check Schema exists. Schema has tapiKey value.
// Authorization: apiKey
if (string.IsNullOrWhiteSpace(apiKey))
{
this.SetContextErrorResult(context);
return;
}
// 4. Validate tenant. Here we could use caching
CustomerModel customer = CustomerService.Find(apiKey);
if (customer == null)
{
this.SetContextErrorResult(context);
return;
}
// 5. Credentials ok, set principal
IPrincipal principal = new GenericPrincipal(new GenericIdentity(apiKey), new string[] { });
context.Principal = principal;
return;
}
public async Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
{
// currently we don't need authentication challenge
return;
}
private void SetContextErrorResult(HttpAuthenticationContext context)
{
context.ErrorResult = new AuthenticationFailedResponse();
}
}
public class AuthenticationFailedResponse : IHttpActionResult
{
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
return Task.FromResult(Execute());
}
private HttpResponseMessage Execute()
{
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized)
{
Content = new StringContent(JsonConvert.SerializeObject(new ApiErrorModel()
{
Message = "Authentication failed",
Description = "Missing or incorrect credentials"
}), Encoding.UTF8, "application/json")
};
return response;
}
}
A colleague of mine found the solution:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class MyAuthenticationFilter : FilterAttribute, IAuthenticationFilter
Upper code has to be added to MyAuthentication. And now we can use it on Controller:
[ApiExceptionFilter]
[RoutePrefix("api/provisioning/v0")]
[MyAuthenticationFilter]
public class ProvisioningController : ApiController
First remove following code from webconfig
config.Filters.Add(new MyAuthenticationFilter()); // Global level
Then add attrbute on controller. And first make sure you have added namespace of that auhthenticationfilter. and extend your class with ActionFilterAttribute and override method onactionexcecution.
I am struggling to simulate the required HttpContext for my unit tests.
I have abstracted control of the session away from my Mvc controller with a SessionManager interface and implemented this with a class called CookieSessionManager. (early development stages).
CookieSessionManager uses the HttpContext by using the injected singleton HttpContextAccessor (in Startup.cs ConfigureServices).
I'm using Cookie Authentication which is setup in Startup.cs with app.UseCookieAuthentication.
Testing this manually in debug mode works as expected
The MSUnit tests I have written for my AccountController class work with a MockSessionManager class injected.
The real issue I have is with the unit tests I have written for my CookieSessionManager class. I have tried to setup the HttpContext as shown below;
[TestClass]
public class CookieSessionManagerTest
{
private IHttpContextAccessor contextAccessor;
private HttpContext context;
private SessionManager sessionManager;
[TestInitialize]
public void Setup_CookieSessionManagerTest()
{
context = new DefaultHttpContext();
contextAccessor = new HttpContextAccessor();
contextAccessor.HttpContext = context;
sessionManager = new CookieSessionManager(contextAccessor);
}
The Error
But the call to sessionManager.Login(CreateValidApplicationUser()); doesn't appear to set the IsAuthenticated flag and the test CookieSessionManager_Login_ValidUser_Authenticated_isTrue fails.
[TestMethod]
public void CookieSessionManager_Login_ValidUser_Authenticated_isTrue()
{
sessionManager.Login(CreateValidApplicationUser());
Assert.IsTrue(sessionManager.isAuthenticated());
}
public ApplicationUser CreateValidApplicationUser()
{
ApplicationUser applicationUser = new ApplicationUser();
applicationUser.UserName = "ValidUser";
//applicationUser.Password = "ValidPass";
return applicationUser;
}
Test Name: CookieSessionManager_Login_ValidUser_Authenticated_isTrue
: line 43 Test Outcome: Failed Test Duration: 0:00:00.0433169
Result StackTrace: at ClaimsWebAppTests.Identity.CookieSessionManagerTest.CookieSessionManager_Login_ValidUser_Authenticated_isTrue()
CookieSessionManagerTest.cs:line 46 Result Message: Assert.IsTrue failed.
MY CODE
SessionManager
using ClaimsWebApp.Models;
namespace ClaimsWebApp.Identity
{
public interface SessionManager
{
bool isAuthenticated();
void Login(ApplicationUser applicationUser);
void Logout();
}
}
CookieSessionManager
using ClaimsWebApp.Identity;
using ClaimsWebApp.Models;
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Security.Claims;
namespace ClaimsWebApp
{
public class CookieSessionManager : SessionManager
{
private List<ApplicationUser> applicationUsers;
private IHttpContextAccessor ContextAccessor;
private bool IsAuthenticated;
public CookieSessionManager(IHttpContextAccessor contextAccessor)
{
this.IsAuthenticated = false;
this.ContextAccessor = contextAccessor;
IsAuthenticated = ContextAccessor.HttpContext.User.Identity.IsAuthenticated;
applicationUsers = new List<ApplicationUser>();
applicationUsers.Add(new ApplicationUser { UserName = "ValidUser" });
}
public bool isAuthenticated()
{
return IsAuthenticated;
}
public void Login(ApplicationUser applicationUser)
{
if (applicationUsers.Find(m => m.UserName.Equals(applicationUser.UserName)) != null)
{
var identity = new ClaimsIdentity(new[] {
new Claim(ClaimTypes.Name, applicationUser.UserName)
},
"MyCookieMiddlewareInstance");
var principal = new ClaimsPrincipal(identity);
ContextAccessor.HttpContext.Authentication.SignInAsync("MyCookieMiddlewareInstance", principal);
IsAuthenticated = ContextAccessor.HttpContext.User.Identity.IsAuthenticated;
}
else
{
throw new Exception("User not found");
}
}
public void Logout()
{
ContextAccessor.HttpContext.Authentication.SignOutAsync("MyCookieMiddlewareInstance");
IsAuthenticated = ContextAccessor.HttpContext.User.Identity.IsAuthenticated;
}
}
}
Startup.cs
using ClaimsWebApp.Identity;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace ClaimsWebApp
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<SessionManager, CookieSessionManager>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationScheme = "MyCookieMiddlewareInstance",
LoginPath = new PathString("/Account/Unauthorized/"),
AccessDeniedPath = new PathString("/Account/Forbidden/"),
AutomaticAuthenticate = true,
AutomaticChallenge = true
});
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Account}/{action=Login}/{id?}");
});
}
}
}
CookieSessionManagerTest.cs
using ClaimsWebApp;
using ClaimsWebApp.Identity;
using ClaimsWebApp.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ClaimsWebAppTests.Identity
{
[TestClass]
public class CookieSessionManagerTest
{
private IHttpContextAccessor contextAccessor;
private HttpContext context;
private SessionManager sessionManager;
[TestInitialize]
public void Setup_CookieSessionManagerTest()
{
context = new DefaultHttpContext();
contextAccessor = new HttpContextAccessor();
contextAccessor.HttpContext = context;
sessionManager = new CookieSessionManager(contextAccessor);
}
[TestMethod]
public void CookieSessionManager_Can_Be_Implemented()
{
Assert.IsInstanceOfType(sessionManager, typeof(SessionManager));
}
[TestMethod]
public void CookieSessionManager_Default_Authenticated_isFalse()
{
Assert.IsFalse(sessionManager.isAuthenticated());
}
[TestMethod]
public void CookieSessionManager_Login_ValidUser_Authenticated_isTrue()
{
sessionManager.Login(CreateValidApplicationUser());
Assert.IsTrue(sessionManager.isAuthenticated());
}
public ApplicationUser CreateValidApplicationUser()
{
ApplicationUser applicationUser = new ApplicationUser();
applicationUser.UserName = "ValidUser";
//applicationUser.Password = "ValidPass";
return applicationUser;
}
public ApplicationUser CreateInValidApplicationUser()
{
ApplicationUser applicationUser = new ApplicationUser();
applicationUser.UserName = "InValidUser";
//applicationUser.Password = "ValidPass";
return applicationUser;
}
}
}
Unfortunately, it's pretty much impossible to test with HttpContext. It's a sealed class that doesn't utilize any interfaces, so you cannot mock it. Usually, your best bet is to abstract away the code that works with HttpContext, and then test just your other, more application-specific code.
It looks like you've sort of already done this via HttpContextAccessor, but you're utilizing it incorrectly. First, you're exposing the HttpContext instance, which pretty much defeats the entire purpose. This class should be able to return something like User.Identity.IsAuthenticated on its own, like: httpContextAccessor.IsAuthenticated. Internally, the property would access the private HttpContext instance and just return the result.
Once you're utilizing it in this way, you can then mock HttpContextAccessor to simply return what you need for your tests, and you don't have to worry about supplying it with an HttpContext instance.
Granted, this means there's still some untested code, namely, the accessor methods that work with HttpContext, but these are generally very straight-forward. For example, the code for IsAuthenticated would just be something like return httpContext.User.Identity.IsAuthenticated. The only way you're going to screw that up is if you fat-finger something, but the compiler will warn you about that.
I created this helper functionality for my unit tests, this allowed me to test those specific methods that required portions of the httpRequest.
public static IHttpContextAccessor GetHttpContext(string incomingRequestUrl, string host)
{
var context = new DefaultHttpContext();
context.Request.Path = incomingRequestUrl;
context.Request.Host = new HostString(host);
//Do your thing here...
var obj = new HttpContextAccessor();
obj.HttpContext = context;
return obj;
}
This doesn't directly answer the context of the question but it provides an alternative method to testing and when you start using it makes life so much easier.
There is an integration testing package available for ASP.NET Core and documentation about it can be found here:
https://docs.asp.net/en/latest/testing/integration-testing.html
Enjoy!
You can create a Test class Like below which inherit the HttpContext. And use the test class where you need it. You can add the missing implementation on the code.
public class TestHttpContext : HttpContext
{
[Obsolete]
public override AuthenticationManager Authentication
{
get { throw new NotImplementedException(); }
}
public override ConnectionInfo Connection
{
get { throw new NotImplementedException(); }
}
public override IFeatureCollection Features
{
get { throw new NotImplementedException(); }
}
public override IDictionary<object, object> Items
{
get { throw new NotImplementedException(); }
set { throw new NotImplementedException(); }
}
public override HttpRequest Request
{
get { throw new NotImplementedException(); }
}
public override CancellationToken RequestAborted
{
get { throw new NotImplementedException(); }
set { throw new NotImplementedException(); }
}
public override IServiceProvider RequestServices
{
get { throw new NotImplementedException(); }
set { throw new NotImplementedException(); }
}
HttpResponse _response;
public override HttpResponse Response
{
get
{
if (this._response == null)
{
this._response = new TestHttpResponse();
this._response.StatusCode = 999;
}
return this._response;
}
}
public override ISession Session
{
get { throw new NotImplementedException(); }
set { throw new NotImplementedException(); }
}
public override string TraceIdentifier
{
get { throw new NotImplementedException(); }
set { throw new NotImplementedException(); }
}
public override ClaimsPrincipal User
{
get { throw new NotImplementedException(); }
set { throw new NotImplementedException(); }
}
public override WebSocketManager WebSockets
{
get { throw new NotImplementedException(); }
}
public override void Abort()
{
throw new NotImplementedException();
}
}
I am trying to access a table using its controller from another controller method.
But when the method tries to call the table controller method I get an exception:
Exception=System.NullReferenceException: Object reference not set to an instance of an object. at Microsoft.WindowsAzure.Mobile.Service.TableController.....
I manage to access the table controller method from the web API and execute it successfully.
I tried the same thing with TodoItem given as an example by the initial mobile service.
After several publishes to the server trying to fix the issue the web API stopped working and I get this exception : An exception of type 'Microsoft.WindowsAzure.MobileServices.MobileServiceInvalidOperationException' occurred in mscorlib.dll but was not handled in user code
Additional information: The request could not be completed. (Internal Server Error) I managed to solve it when I reopened a mobile service and database with the exact same code that didn't work.
Any tips ?
Here is my table controller created by the controller wizard:
using System.Linq;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.OData;
using Microsoft.WindowsAzure.Mobile.Service;
using FringProjectMobileService.DataObjects;
using FringProjectMobileService.Models;
namespace FringProjectMobileService.Controllers
{
public class StorageItemController : TableController<StorageItem>
{
protected override void Initialize(HttpControllerContext controllerContext)
{
base.Initialize(controllerContext);
FringProjectMobileServiceContext context = new FringProjectMobileServiceContext();
DomainManager = new EntityDomainManager<StorageItem>(context, Request, Services);
}
// GET tables/StorageItem
public IQueryable<StorageItem> GetAllStorageItem()
{
return Query();
}
// GET tables/StorageItem/xxxxxxxxxx
public SingleResult<StorageItem> GetStorageItem(string id)
{
return Lookup(id);
}
// PATCH tables/StorageItem/xxxxxxxx
public Task<StorageItem> PatchStorageItem(string id, Delta<StorageItem> patch)
{
return UpdateAsync(id, patch);
}
// POST tables/StorageItem
public async Task<IHttpActionResult> PostStorageItem(StorageItem item)
{
StorageItem current = await InsertAsync(item);
return CreatedAtRoute("Tables", new { id = current.Id }, current);
}
// DELETE tables/StorageItem/xxxxxxxxxx
public Task DeleteStorageItem(string id)
{
return DeleteAsync(id);
}
}
}
Below the other controller code trying to access the method:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using Microsoft.WindowsAzure.Mobile.Service;
namespace FringProjectMobileService.Controllers
{
public class ArduinoController : ApiController
{
public ApiServices Services { get; set; }
// GET api/Arduino
public string Get()
{
Services.Log.Info("Hello from custom controller!");
return "Hello";
}
public async void PostProcessTag(String id)
{
Microsoft.WindowsAzure.MobileServices.MobileServiceClient client = new Microsoft.WindowsAzure.MobileServices.MobileServiceClient("http://some-service.azure-mobile.net", "XXXXXXXXXXXXXXX");
Microsoft.WindowsAzure.MobileServices.IMobileServiceTable<DataObjects.StorageItem> storage_item_table = client.GetTable<DataObjects.StorageItem>();
await storage_item_table.ToEnumerableAsync();
}
}
}
I also tried a different implementation for the method :
public void PostProcessTag(String id)
{
StorageItemController table_controller = new StorageItemController();
IQueryable<DataObjects.StorageItem> item = table_controller.GetAllStorageItem();
}
The service context:
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;
using System.Linq;
using Microsoft.WindowsAzure.Mobile.Service;
using Microsoft.WindowsAzure.Mobile.Service.Tables;
namespace FringProjectMobileService.Models
{
public class FringProjectMobileServiceContext : DbContext
{
// You can add custom code to this file. Changes will not be overwritten.
//
// If you want Entity Framework to alter your database
// automatically whenever you change your model schema, please use data migrations.
// For more information refer to the documentation:
// http://msdn.microsoft.com/en-us/data/jj591621.aspx
//
// To enable Entity Framework migrations in the cloud, please ensure that the
// service name, set by the 'MS_MobileServiceName' AppSettings in the local
// Web.config, is the same as the service name when hosted in Azure.
private const string connectionStringName = "Name=MS_TableConnectionString";
public FringProjectMobileServiceContext() : base(connectionStringName)
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
string schema = ServiceSettingsDictionary.GetSchemaName();
if (!string.IsNullOrEmpty(schema))
{
modelBuilder.HasDefaultSchema(schema);
}
modelBuilder.Conventions.Add(
new AttributeToColumnAnnotationConvention<TableColumnAttribute, string>(
"ServiceTableColumn", (property, attributes) => attributes.Single().ColumnType.ToString()));
}
public System.Data.Entity.DbSet<FringProjectMobileService.DataObjects.StorageItem> StorageItems { get; set; }
}
}
Shibboleth is a SSO Authentication that is added to IIS as a "plugin".
After a user has done a Login there are Headers showing the Shibboleth Session:
ShibSessionID
ShibIdentityProvider
eppn
affiliation
entitlement
unscopedaffiliation
...more
So i can extract username and roles from the Headers.
so far so fine.
Question:
How can I implement a handler that does read the headers and set the status that a user is authorized?
Idea is to use the
[Authorize]
Attribute and the Method
Roles.IsUserInRole.
All from the Headers, no Database, no User Management.
Update
Implementation According to the Answer from #Pharylon
In this Update there is nothing new, just a help for the copy&past friends.
Of course you have to adjust the properties and Header fieldnames according to your Shibboleth Setup.
File: ShibbolethPrincipal.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Security.Principal; //GenericPrincipal
namespace Shibboleth
{
public class ShibbolethPrincipal : GenericPrincipal
{
public string username
{
get { return this.Identity.Name.Replace("#ksz.ch", ""); }
}
public string firstname
{
get { return HttpContext.Current.Request.Headers["givenName"]; }
}
public string lastname
{
get { return HttpContext.Current.Request.Headers["surname"]; }
}
public string phone
{
get { return HttpContext.Current.Request.Headers["telephoneNumber"]; }
}
public string mobile
{
get { return HttpContext.Current.Request.Headers["mobile"]; }
}
public string entitlement
{
get { return HttpContext.Current.Request.Headers["eduzgEntitlement"]; }
}
public string homeOrganization
{
get { return HttpContext.Current.Request.Headers["homeOrganization"]; }
}
public DateTime birthday
{
get
{
DateTime dtHappy = DateTime.MinValue;
try
{
dtHappy = DateTime.Parse(HttpContext.Current.Request.Headers["dateOfBirth"]);
}
finally
{
}
return dtHappy;
}
set {}
}
public ShibbolethPrincipal()
: base(new GenericIdentity(GetUserIdentityFromHeaders()), GetRolesFromHeader())
{
}
public static string GetUserIdentityFromHeaders()
{
//return HttpContext.Current.Request.Headers["eppn"];
return HttpContext.Current.Request.Headers["principalName"];
}
public static string[] GetRolesFromHeader()
{
string[] roles = null;
//string rolesheader = HttpContext.Current.Request.Headers["affiliation"];
string rolesheader = HttpContext.Current.Request.Headers["eduzgEntitlement"];
if (rolesheader != null)
{
roles = rolesheader.Split(';');
}
return roles;
}
}
}
File: ShibbolethController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace Shibboleth
{
public class ShibbolethController : Controller
{
protected new ShibbolethPrincipal User
{
get
{
return (base.User as ShibbolethPrincipal) ?? null; //CustomPrincipal.GetUnauthorizedPrincipal();
}
}
}
}
File: Global.asax
void Application_PostAuthenticateRequest(object sender, EventArgs e)
{
var ctx = HttpContext.Current;
var principal = new ShibbolethPrincipal();
HttpContext.Current.User = principal;
}
Using examples:
namespace itservices.Controllers
{
[Authorize] //examples : [Authorize(Roles="Administrators")], [Authorize(Users="Alice,Bob")]
public class PasswordMailController : ShibbolethController
{
if(User.IsInRole("staff"))
{
You'll want to create a method in Global.asax.cs that has the following signature
protected void Application_PostAuthenticateRequest()
{
//Your code here.
}
This will be called automatically before almost anything else is done (MVC will call this method if it exists, you don't have to "turn it on" anywhere), and this is where you need to set the Principal. For instance, let's assume you have a header called RolesHeader that has a comma separated value of roles and another header called UserId that has (duh) the user ID.
Your code, without any error handling, might look something like:
protected void Application_PostAuthenticateRequest()
{
var rolesheader = Context.Request.Headers["RolesHeader"];
var userId = Context.Request.Headers["UserId"];
var roles = rolesheader.Split(',');
var principal = new GenericPrincipal(new GenericIdentity(userId), roles);
Context.User = principal;
}
It's the Principal/Identity that the [Authorize] attribute uses, so setting it here at the beginning of the request lifecycle means the [Authorize] attribute will work correctly.
The rest of this is optional, but I recommend it:
I like to create my own custom classes that implement IPrincipal and IIdentity instead of using the GenericPrincipal and GenericIdentity, so I can stuff more user information in it. My custom Principal and Identity objects then have much more rich information, such as branch numbers or email addresses or whatever.
Then, I create a Controller called BaseController that has the following
protected new CustomPrincipal User
{
get
{
return (base.User as CustomPrincipal) ?? CustomPrincipal.GetUnauthorizedPrincipal();
}
}
This allows me to access all my rich, custom Principal data instead of just what's defined in IPrincipal. All of my real controllers then inherit from BaseController instead of directly from Controller.
Obviously, when using a custom Principal like this, in the Application_PostAuthenticateRequest() method, you'd set the Context.User to be your CustomPrincipal instead of a GenericPrincipal.