I'm trying to define a permissions for a ServiceStack Service which only can access the Admin Role for example and I have this Service with the RequireRole attribute but it seems does not work because I can access the service as a USER .
[Authenticate]
[RequiredRole("Admin")]
public class HelloService : Service
{
public const string HelloServiceCounterKey = "HelloServiceCounter";
public object Any(HelloRequest request)
{
var userSession = SessionAs<AppHost.CustomUserSession>();
Session.Set(HelloServiceCounterKey, Session.Get<int>(HelloServiceCounterKey) + 1);
var roles = string.Join(", ", userSession.Roles.ToArray());
return new HelloResponse { Result = "Hello, " + request.Name + ", your role(s): " + roles };
}
}
AccountController.cs
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginModel model, string returnUrl)
{
if (ModelState.IsValid)
{
try
{
if (!WebSecurity.UserExists("Admin"))
WebSecurity.CreateUserAndAccount("admin", "abc");
var authService = AppHostBase.Resolve<AuthService>();
authService.RequestContext = System.Web.HttpContext.Current.ToRequestContext();
var response = authService.Authenticate(new Auth
{
UserName = model.UserName,
Password = model.Password,
RememberMe = model.RememberMe
});
// add ASP.NET auth cookie
FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
return RedirectToLocal(returnUrl);
}
catch (HttpError)
{
}
}
and Here's my AppHost.cs
public override void Configure(Funq.Container container)
{
/*Register storage for User Session */
container.Register<ICacheClient>(new MemoryCacheClient()); /*Tipo Base de MemoryCacheClient es ICacheClient*/
container.Register<ISessionFactory>(c => new SessionFactory(c.Resolve<ICacheClient>())); /*Tipo Base de SessionFactory es ISessionFactory*/
Plugins.Add(new AuthFeature(
() => new CustomUserSession(),
new[] { new CustomCredentialsAuthProvider() }
));
Plugins.Add(new SessionFeature());
Routes
.Add<HelloService>("/hello")
.Add<HelloService>("/hello/{Name*}");
//Set JSON web services to return idiomatic JSON camelCase properties
ServiceStack.Text.JsConfig.EmitCamelCaseNames = true;
container.Register(new TodoRepository());
//Set MVC to use the same Funq IOC as ServiceStack
ControllerBuilder.Current.SetControllerFactory(new FunqControllerFactory(container));
}
The wiki states:
As with Authenticate, you can mark services (instead of DTO) with
RequiredPermission attribute, too.
It does NOT state whether you can use the RequiredRole attribute with a service, so I think you cannot and looking at the comments in source it does seem to target just requestDTO object.
Related
I want to configure ADFS endpoints in my asp.net app at runtime.
There is a problem: if I declare single callback method for multiple endpoints then I have exception:
Microsoft.IdentityModel.Tokens.SecurityTokenSignatureKeyNotFoundException: IDX10501: Signature validation failed. Unable to match keys:
kid: '[PII is hidden]',
token: '[PII is hidden]'.
If I will hard-code callbacks (Wreply) for each endpoint then all works, but this is not my case.
Startup.cs
public class Startup
{
public void Configuration(IAppBuilder app)
{
var federationEndpoints = Service.ListActiveFederationEndpoints();
if (federationEndpoints.Any())
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
var endpointOptions = new List<WsFederationAuthenticationOptions>();
foreach (var endpoint in federationEndpoints)
{
string metadata = endpoint.ServerUri;
string wtrealm = endpoint.RelyingPartyIdentifier;
endpointOptions.Add(new WsFederationAuthenticationOptions
{
Wtrealm = wtrealm,
MetadataAddress = metadata,
AuthenticationType = endpoint.Name
});
}
app.Map("/FederationAuth", configuration =>
{
endpointOptions.ForEach(o => app.UseWsFederationAuthentication(o));
});
}
AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.Name;
}
}
Login and common callback (Wreply) in FederationAuthController
[AllowAnonymous]
public void ExternalLogin(string endpointName)
{
var ctx = Request.GetOwinContext();
ctx.Authentication.Challenge(
new AuthenticationProperties { RedirectUri = Url.Action("LoginCallbackAdfs", "FederationAuth") },
endpointName);
}
public ActionResult LoginCallbackAdfs()
{
var ctx = System.Web.HttpContext.Current;
var claimsIdentity = User.Identity as ClaimsIdentity;
var sessionIdentity = Service.LoginByClaims(claimsIdentity);
return this.RedirectToAction("Index", "SinglePage");
}
I've read many answers for configuring hard-coded multiple ADFS endpoints in Web.config but is there possibility to configure enpoints at runtime?
Thank you!
Wreply should be unique and set for each federation middleware during pipeline building. I made unique Wreply including endpoint name as callback parameter.
Startup.cs
public void Configuration(IAppBuilder app)
{
var federationChannels = Service.GetFederationChannels();
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.SetDefaultSignInAsAuthenticationType(CookieAuth.AuthenticationType);
foreach (var channel in federationChannels)
{
var metadata = channel.Metadata;
var wtrealm = channel.Wtrealm ;
var host = GetServerAddress();
var wreply = $"{host}FederationLogin/channel={channel.Id}";
app.UseWsFederationAuthentication(new WsFederationAuthenticationOptions
{
Wtrealm = wtrealm,
MetadataAddress = metadata,
AuthenticationType = channel.Id,
Wreply = wreply,
SignOutWreply = host
});
}
AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.Name;
}
Controller
public ActionResult FederationLogin(string channel)
{
....
HttpContext.GetOwinContext().Authentication.Challenge(new AuthenticationProperties(), channel);
....
}
I added the RazorPlugin along with a Test.cshtml in the root View folder (tried wwwroot as well along with TestGet.cshtml). This is an donet Core project. Whenever browsing to: /api/test the following error is generated:
Snapshot of TestGet generated by ServiceStack on 6/3/2017 4:20:40 PM
view json datasource from original url:
http://localhost:51550/api/test? in other formats: json xml csv jsv
Response Status Error CodeNullReferenceExceptionMessageObject
reference not set to an instance of an object.Stack Trace[TestGet:
6/3/2017 4:20:40 PM]: [REQUEST: {}] System.NullReferenceException:
Object reference not set to an instance of an object. at
ServiceStack.Mvc.RazorFormat.FindView(IEnumerable1 viewNames) in
/opt/lib/teamcity-agent/work/d09206570215629/src/ServiceStack.Mvc/RazorFormat.cs:line
174 at ServiceStack.Mvc.RazorFormat.ProcessRequest(IRequest req,
IResponse res, Object dto) in
/opt/lib/teamcity-agent/work/d09206570215629/src/ServiceStack.Mvc/RazorFormat.cs:line
138 at System.Linq.Enumerable.Any[TSource](IEnumerable1 source,
Func`2 predicate) at
ServiceStack.Formats.HtmlFormat.SerializeToStream(IRequest req, Object
response, IResponse res) in
/opt/lib/teamcity-agent/work/d09206570215629/src/ServiceStack/Formats/HtmlFormat.cs:line
63Errors
The other formats work (json/etc).
public class Test
{
public string ExternalId { get; set; }
}
[Authenticate, Route("/test")]
public class TestGet : IGet, IReturn<Test>
{
}
public class TestService : Service
{
public IAutoQueryDb _autoQueryDb { get; set; }
public IDbConnectionFactory _dbFactory { get; set; }
public TestService(IDbConnectionFactory dbFactory)
{
_dbFactory = dbFactory;
}
public Test Get(TestGet request)
{
var test = new Test();
test.ExternalId = "abc";
return test;
}
}
Test.cshtml
#using App.Shared.DomainModel
#model Test
<div>Test</div>
AppHost.cs
using App.DomainServices;
using App.FontEnd.Infrastructure.Configuration;
using App.FontEnd.Infrastructure.Core;
using App.Shared.DomainModel;
using Funq;
using LightInject;
using ServiceStack;
using ServiceStack.Admin;
using ServiceStack.Api.Swagger;
using ServiceStack.Auth;
using ServiceStack.Caching;
using ServiceStack.Configuration;
using ServiceStack.Data;
using ServiceStack.Mvc;
using ServiceStack.OrmLite;
using ServiceStack.Validation;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
namespace App.FrontEnd
{
public class AppHost : AppHostBase
{
/// <summary>
/// Base constructor requires a Name and Assembly where web service implementation is located
/// </summary>
public AppHost()
: base("TestApi", typeof(CompanyService).GetAssembly())
{
}
/// <summary>
/// Application specific configuration
/// This method should initialize any IoC resources utilized by your web service classes.
/// </summary>
public override void Configure(Container container)
{
this.GlobalRequestFilters.Add((httpReq, httpResp, requestDto) =>
{
var currentSession = httpReq.GetSession();
if (currentSession != null)
{
RequestContext.Instance.Items.Add("CurrentUserName", currentSession.UserName);
RequestContext.Instance.Items.Add("CurrentUserId", currentSession.UserAuthId);
}
});
ServiceContainer LightContainer = new ServiceContainer();
LightContainer.RegisterInstance<IDbConnectionFactory>
(
new OrmLiteConnectionFactory(
"removed",
SqlServer2014Dialect.Provider
)
);
LightContainer.Register<IDbConnection>(c =>
c.GetInstance<IDbConnectionFactory>().OpenDbConnection(),
new PerScopeLifetime()
);
LightContainer.Register<OrmLiteAppSettings>(c =>
new OrmLiteAppSettings(c.GetInstance<IDbConnectionFactory>()));
LightContainer.Register<ServiceStack.Web.IServiceGatewayFactory>(x => new ApiServiceGatewayFactory());
container.Adapter = new LightInjectAdapter(LightContainer);
var settings = LightContainer.GetInstance<OrmLiteAppSettings>();
settings.InitSchema();
AppSettings = new MultiAppSettings(
settings
);
container.Register<ICacheClient>(new OrmLiteCacheClient
{
DbFactory = LightContainer.GetInstance<IDbConnectionFactory>()
});
var cacheclient = container.Resolve<ICacheClient>();
cacheclient.InitSchema();
AuthConfig(container, AppSettings);
Plugins.Add(new RegistrationFeature());
Plugins.Add(new SwaggerFeature());
Plugins.Add(new RequestLogsFeature());
Plugins.Add(new PostmanFeature());
Plugins.Add(new CorsFeature(allowCredentials: true));
Plugins.Add(new ValidationFeature());
Plugins.Add(new RazorFormat());
OrmLiteConfig.InsertFilter = (dbCmd, row) =>
{
var auditRow = row as CoreModel;
if (auditRow != null)
{
var currentDate = DateTime.UtcNow;
var insertUserId = RequestContext.Instance.Items["CurrentUserId"] as string;
auditRow.Id = Guid.NewGuid();
auditRow.CreatedDate = currentDate;
auditRow.CreatedBy = insertUserId;
auditRow.UpdatedDate = currentDate;
auditRow.UpdatedBy = insertUserId;
}
};
OrmLiteConfig.UpdateFilter = (dbCmd, row) =>
{
var auditRow = row as CoreModel;
if (auditRow != null)
{
var updateUserId = RequestContext.Instance.Items["CurrentUserId"] as string;
auditRow.UpdatedDate = DateTime.UtcNow;
auditRow.UpdatedBy = updateUserId;
}
};
var aq = new AutoQueryFeature { MaxLimit = 100, EnableAutoQueryViewer = true };
aq.ImplicitConventions.Add("%neq", aq.ImplicitConventions["%NotEqualTo"]);
aq.ImplicitConventions.Add("%eq", "{Field} = {Value}");
Plugins.Add(aq);
Plugins.Add(new AdminFeature());
SetConfig(new HostConfig
{
HandlerFactoryPath = "api",
DebugMode = true
});
container.CheckAdapterFirst = true;
//Set up service stack validators
container.ValidatorsSetup();
}
public void AuthConfig(Container container, IAppSettings settings)
{
Plugins.Add(new AuthFeature(() => new AuthUserSession(),
new IAuthProvider[] {
new CredentialsAuthProvider(AppSettings),
new JwtAuthProvider(AppSettings)
{
AuthKeyBase64 = "abcdefgh"
},
new BasicAuthProvider()
}));
var authRepo = CreateOrmLiteAuthRepo(container, settings);
}
private static IUserAuthRepository CreateOrmLiteAuthRepo(Container container, IAppSettings appSettings)
{
//Store User Data into the referenced SqlServer database
container.Register<IAuthRepository>(c =>
new OrmLiteAuthRepository(c.Resolve<IDbConnectionFactory>()));
//Use OrmLite DB Connection to persist the UserAuth and AuthProvider info
var authRepo = (OrmLiteAuthRepository)container.Resolve<IAuthRepository>();
//If using and RDBMS to persist UserAuth, we must create required tables
if (appSettings.Get("RecreateAuthTables", false))
authRepo.DropAndReCreateTables(); //Drop and re-create all Auth and registration tables
else
authRepo.InitSchema(); //Create only the missing tables
return authRepo;
}
}
}
I've created a minimal verifiable MVC test project to simulate your configuration which is working as expected at: https://github.com/NetCoreApps/scratch/tree/master/src/RazorApi
It includes the same Test Services, (sans [Authenticate] attribute):
public class Test
{
public string ExternalId { get; set; }
}
[Route("/test")]
public class TestGet : IGet, IReturn<Test>
{
}
public class TestService : Service
{
public Test Get(TestGet request)
{
var test = new Test { ExternalId = "abc" };
return test;
}
}
And the minimal AppHost configuration which just sets the HandlerFactoryPath to api and registers ServiceStack's RazorFormat plugin:
public class AppHost : AppHostBase
{
public AppHost()
: base("ServiceStack + .NET Core", typeof(MyServices).GetTypeInfo().Assembly) {}
public override void Configure(Funq.Container container)
{
SetConfig(new HostConfig
{
HandlerFactoryPath = "api",
DebugMode = true,
});
Plugins.Add(new RazorFormat());
}
}
With the Test.cshtml maintained in /Views/Test.cshtml:
#model Test
#{
Layout = "_Layout";
}
<h1>Test.cshtml</h1>
<p>#Model.ExternalId</p>
Which works as expected with the Razor View executed when calling:
http://localhost:5000/api/test
Which also works when renamed to match the Request DTO at TestGet.cshtml.
As the issue seems specific to your project I'd compare your layout with bare RazorApi Github Project to see if you can find any differences, failing that I'd recommend commenting out configuration to get it to a working state then uncomment sections at a time to find out which configuration is causing the issue.
I have a self hosted service stack app in which I'm using Facebook Oath and Redis - the Facebook and redis side of things seem to be working ie. when I visit
abc.com/auth/facebook
The custom user session gets populated in OnAuthenticated method. The Redis cache has the data persisted correctly..so far so good
The problem Im having is understanding how to retrieve this CustomUserSession in a subsequent request. To begin with the oauth redirect page "/About-Us" is where I want to retrieve the session value however it is always null
[DataContract]
public class CustomUserSession : AuthUserSession
{
[DataMember]
public string CustomId { get; set; }
public override void OnAuthenticated(IServiceBase authService, IAuthSession session, IAuthTokens tokens, Dictionary<string, string> authInfo)
{
// receiving session id here and can retrieve from redis cache
}
public override bool IsAuthorized(string provider)
{
// when using the [Authenticate] attribute - this Id is always
// a fresh value and so doesn't exist in cache and cannot be auth'd
string sessionKey = SessionFeature.GetSessionKey(this.Id);
cacheClient = ServiceStackHost.Instance.TryResolve<ICacheClient>();
CustomUserSession session = cacheClient.Get<CustomUserSession>(sessionKey);
if (session == null)
{
return false;
}
return session.IsAuthenticated;
}
}
[DefaultView("AboutUs")]
public class AboutUsService : AppServiceBase
{
public object Get(AboutUsRequest request)
{
var sess = base.UserSession;
return new AboutUsResponse
{
//custom Id is always null??
Name = sess.CustomId
};
}
}
public abstract class AppServiceBase : Service
{
protected CustomUserSession UserSession
{
get
{
return base.SessionAs<CustomUserSession>();
}
}
}
How I register the cache & session etc.
AppConfig = new AppConfig(appSettings);
container.Register(AppConfig);
container.Register<IRedisClientsManager>(c => new PooledRedisClientManager("10.1.1.10:6379"));
container.Register(c =>
c.Resolve<IRedisClientsManager>().GetCacheClient());
ConfigureAuth(container, appSettings);
the contents of ConfigureAuth()
var authFeature = new AuthFeature(
() => new CustomUserSession(),
new IAuthProvider[]
{
new FacebookAuthProvider(appSettings), // override of BasicAuthProvider
}
) {HtmlRedirect = null, IncludeAssignRoleServices = false};
Plugins.Add(authFeature);
I feel I'm missing something obvious here.... thanks in advance
To register to use a CustomUserSession for your typed Sessions, it needs to be specified when you register the AuthFeature, e.g:
Plugins.Add(new AuthFeature(() => new CustomUserSession(), ...));
Here is the appHost configuration code:
OrmLiteConfig.DialectProvider = PostgreSQLDialectProvider.Instance;
var dbFactory = new OrmLiteConnectionFactory();
dbFactory.RegisterConnection("NamedKeyConnOne", new OrmLiteConnectionFactory("ConnOne"))
{
ConnectionFilter = x => new ProfiledDbConnection(x, Profiler.Current)
});
dbFactory.RegisterConnection("NamedKeyConnTwo", new OrmLiteConnectionFactory("ConnTwo")
{
ConnectionFilter = x => new ProfiledDbConnection(x, Profiler.Current)
});
container.Register<IDbConnectionFactory>(dbFactory);
and here is the authentication portion:
container.Register<IUserAuthRepository>(c => new OrmLiteAuthRepository(c.Resolve<IDbConnectionFactory>())); //Authentication and authorization
container.Resolve<IUserAuthRepository>().InitSchema();
So my question is "How do you go about passing the correct IDbConnectionFactory when there is no default connection string?"
Thank you,
Stephen
You can't inject a named IDbConnection connection but you can resolve it from the IDbConnectionFactory which you can access from your services like:
public class MyServices : Service
{
public IDbConnectionFactory DbFactory { get; set; }
public object Any(Request request)
{
using (var db = DbFactory.OpenDbConnection("NamedKeyConnOne"))
{
//...
}
}
}
I have read the documentation and have successfully implemented a custom authentication layer like below:
public class SmartLaneAuthentication : CredentialsAuthProvider
{
private readonly SmartDBEntities _dbEntities;
public SmartLaneAuthentication(SmartDBEntities dbEntities)
{
_dbEntities = dbEntities;
}
public override bool TryAuthenticate(IServiceBase authService, string userName, string password)
{
var user = _dbEntities.Users.FirstOrDefault(x => !((bool)x.ActiveDirectoryAccount) && x.UserName == userName);
if (user == null) return false;
// Do my encryption, code taken out for simplicity
return password == user.Password;
}
public override void OnAuthenticated(IServiceBase authService, IAuthSession session, IOAuthTokens tokens, Dictionary<string, string> authInfo)
{
// user should never be null as it's already been authenticated
var user = _dbEntities.Users.First(x => x.UserName == session.UserAuthName);
var customerCount = _dbEntities.Customers.Count();
session.UserName = user.UserName;
session.DisplayName = user.DisplayName;
session.CustomerCount = customerCount; // this isn't accessible?
authService.SaveSession(session, SessionExpiry);
}
}
I then register it in AppHost:
Plugins.Add(new AuthFeature(() => new SmartLaneUserSession(),
new IAuthProvider[]
{
new SmartLaneAuthentication(connection)
})
{
HtmlRedirect = null
});
Plugins.Add(new SessionFeature());
Notice I'm using a SmartLaneUserSession like below, where I have added a Custom Property called CustomerCount:
public class SmartLaneUserSession : AuthUserSession
{
public int CustomerCount { get; set; }
}
When I try and access this property to set it in the OnAuthenticated method of my SmartLaneAuthentication class, it isn't accessible. How would I access and set this property when the user is logged in?
In the OnAuthenticated method you will need to cast the session (of type IAuthSession) into your session object type, such as:
...
var customerCount = _dbEntities.Customers.Count();
var smartLaneUserSession = session as SmartLaneUserSession;
if(smartLaneUserSession != null)
{
smartLaneUserSession.UserName = user.UserName;
smartLaneUserSession.DisplayName = user.DisplayName;
smartLaneUserSession.CustomerCount = customerCount; // Now accessible
// Save the smartLaneUserSession object
authService.SaveSession(smartLaneUserSession, SessionExpiry);
}
In your service you can access the session using the SessionAs<T> method. So in your case you can use:
public class MyService : Service
{
public int Get(TestRequest request)
{
var session = SessionAs<SmartLaneUserSession>();
return session.CustomerCount;
}
}