How do I use my custom ServiceStack authentication provider with Redis? - redis

I have implemented a custom CredentialsAuthProvider for my authentication and used it with the default in memory session storage.
Now I tried to change the session storage to Redis and added this to my Configure() method in the AppHost:
container.Register<IRedisClientsManager>(c =>
new PooledRedisClientManager("localhost:6379"));
container.Register<ICacheClient>(c => (ICacheClient)c
.Resolve<IRedisClientsManager>()
.GetCacheClient()).ReusedWithin(Funq.ReuseScope.None);
Now when I authenticate, I can see that a key with urn:iauthsession:... is added to my Redis server. But all routes with the [Authenticate] attribute give a 401 Unauthorized error.
The CustomCredentialsAuthProvider is implemented like this:
public class CustomCredentialsAuthProvider : CredentialsAuthProvider
{
public override bool TryAuthenticate(IServiceBase authService, string userName, string password)
{
if (userName != string.Empty && password != string.Empty)
{
// Database call ...
var session = (CustomSession)authService.GetSession();
session.ClientId = login.ClientId;
// Fill session...
authService.SaveSession(session, SessionExpiry);
return true;
}
return false;
}
}
ServiceStack Version: 3.9.71
EDIT :
I tried to override the CredentialsAuthProvider IsAuthorized method but without success.
But I'm inheriting my session object from AuthUserSession, which also has a IsAuthorized method. When I return true from this method the Redis session does work with the Authenticate Attribute.
public class CustomSession : AuthUserSession
{
public int ClientId { get; set; }
...
public override bool IsAuthorized(string provider)
{
return true;
}
}

The Authenticate attribute calls the IsAuthorized of the AuthUserSession class.
In my case to make it work with the Redis cache client, I've done the following
public override bool IsAuthorized(string provider)
{
string sessionKey = SessionFeature.GetSessionKey(this.Id);
ICacheClient cacheClient = AppHostBase.Resolve<ICacheClient>();
CustomUserSession session = cacheClient.Get<CustomUserSession>(sessionKey);
if (session == null)
{
return false;
}
return session.IsAuthenticated;
}

I couldn't figure out a way to get the [Authenticate] Attribute to work with Redis storage.
I had to write a custom [SessionAuth] Attribute
public class SessionAuthAttribute : RequestFilterAttribute
{
public ICacheClient cache { get; set; }
public string HtmlRedirect { get; set; }
public SessionAuthAttribute()
{
}
public override void Execute(IHttpRequest req, IHttpResponse res, object requestDto)
{
string sessionId = req.GetSessionId();
if (string.IsNullOrEmpty(sessionId))
{
HandleNoSession(req, res);
}
else
{
var session = cache.Get<CustomSession>("urn:iauthsession:" + sessionId);
if (session == null || !session.IsAuthenticated)
{
HandleNoSession(req, res);
}
}
}
private void HandleNoSession(IHttpRequest req, IHttpResponse res)
{
if (req.ResponseContentType.MatchesContentType(MimeTypes.Html))
{
res.RedirectToUrl(HtmlRedirect);
res.End();
}
res.StatusCode = (int)HttpStatusCode.Unauthorized;
res.Write("not authorized");
res.Close();
}
}
In my AppHost Configure() method I just register the SessionFeature and the IRedisClientsManager/ICacheClient:
Plugins.Add(new SessionFeature());
container.Register<IRedisClientsManager>(c => new PooledRedisClientManager("localhost:6379"));
container.Register<ICacheClient>(c => (ICacheClient)c.Resolve<IRedisClientsManager>()
.GetCacheClient()).ReusedWithin(Funq.ReuseScope.None);
The CustomSession class inherits from AuthUserSession:
public class CustomSession : AuthUserSession
{
public int ClientId { get; set; }
...
}
And I have a normal service route on /login/auth for the authentication part and a /login/logout route to remove the session:
public class LoginService : Service
{
public ICacheClient cache { get; set; }
public object Post(AuthRequest request)
{
string userName = request.UserName;
string password = request.Password;
// check login allowed
if (IsAllowed)
{
var session = SessionFeature.GetOrCreateSession<CustomSession>(cache);
session.ClientId = login.ClientId;
...
session.IsAuthenticated = true;
session.Id = SessionFeature.GetSessionId();
this.SaveSession(session, TimeSpan.FromSeconds(30 * 60));
return true;
}
return false;
}
[SessionAuth]
public object Any(LogoutRequest request)
{
this.RemoveSession();
return true;
}
}
}
I'm still interested in a solution that works with the normal [Authenticate] Attribute.

Related

FluentValidation failure not returning BadRequest

I have wired up FluentValidation as per instructions, and when debuging test I can see that model is invalid based on the test setup, but exception is not thrown, but rather method on the controller is being executed. This is on 3.1 with EndPoint routing enabled. Is there anything else one needs to do to get this to work and throw. What happens is that validation obviously runs; it shows as ModelState invalid and correct InstallmentId is invalid, but it keeps processing in Controller instead of throwing exception.
services.AddMvc(
options =>
{
options.EnableEndpointRouting = true;
//// options.Filters.Add<ExceptionFilter>();
//// options.Filters.Add<CustomerRequestFilter>();
})
.AddFluentValidation(
config =>
{
config.RegisterValidatorsFromAssemblyContaining<Startup>();
})
Command and Validator
public class ProcessManualPayment
{
public class Command
: CustomerRequest<Result?>
{
public Guid PaymentPlanId { get; set; }
public Guid InstallmentId { get; set; }
public Guid PaymentCardId { get; set; }
}
public class Validator : AbstractValidator<Command>
{
public Validator()
{
this.RuleFor(x => x.CustomerId)
.IsValidGuid();
this.RuleFor(x => x.PaymentPlanId)
.IsValidGuid();
this.RuleFor(x => x.InstallmentId)
.IsValidGuid();
this.RuleFor(x => x.PaymentCardId)
.IsValidGuid();
}
}
Controller
[Authorize]
[HttpPost]
[Route("payments")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> ProcessManualPayment(
[FromBody]
ProcessManualPayment.Command command)
{
Test
[Fact]
public async Task When_Command_Has_Invalid_Payload_Should_Fail()
{
var client = this.factory.CreateClient();
// Arrange
var validCmd = new ProcessManualPayment.Command()
{
CustomerId = Guid.NewGuid(),
PaymentPlanId = Guid.NewGuid(),
InstallmentId = Guid.NewGuid(),
PaymentCardId = Guid.NewGuid(),
};
var validCmdJson = JsonConvert.SerializeObject(validCmd, Formatting.None);
var jObject = JObject.Parse(validCmdJson);
jObject["installmentId"] = "asdf";
var payload = jObject.ToString(Formatting.None);
// Act
var content = new StringContent(payload, Encoding.UTF8, MediaTypeNames.Application.Json);
var response = await client.PostAsync(MakePaymentUrl, content);
var returned = await response.Content.ReadAsStringAsync();
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
}
[Fact]
public async Task When_Payload_Is_Null_Should_Fail()
{
// Arrange
var client = this.factory.CreateClient();
// Act
var response = await client.PostAsJsonAsync(MakePaymentUrl, null);
// Assert
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
}
GuidValidator
public class GuidValidator : PropertyValidator
{
public GuidValidator()
: base("'{PropertyName}' value {AttemptedValue} is not a valid Guid.")
{
}
protected override bool IsValid(PropertyValidatorContext context)
{
context.MessageFormatter.AppendArgument("AttemptedValue", context.PropertyValue ?? "'null'");
if (context.PropertyValue == null)
{
return false;
}
Guid.TryParse(context.PropertyValue.ToString(), out var value);
return IsValid(value);
}
private static bool IsValid(Guid? value) =>
value.HasValue
&& !value.Equals(Guid.Empty);
}
Mystery solved, I was missing [ApiController] attribute on the controller.

.NET Core 3 Service result to Controller to FE (data or error reason)

building new app on .net core 3 and Angular. Overall all works, but I want to add more intelligence to service/controller part. This is one of the api's, but this logic can be applied to others as as well.
Here's my Login Controller:
[HttpPost]
public async Task<IActionResult> Login([FromBody] UserLoginDto userLogin)
{
var token = await _userService.LoginAsync(userLogin);
if (token != null)
{
return Ok(token);
}
else
{
return BadRequest("Something went wrong");
}
}
And here's my userService:
public async Task<string> LoginAsync(UserLoginDto userLogin)
{
var user = await _userManager.FindByEmailAsync(userLogin.Email);
if (user != null)
{
var result = await _signInManager.PasswordSignInAsync(user, userLogin.Password, false, true);
if (result.Succeeded)
{
var roles = await _userManager.GetRolesAsync(user);
var tokenJson = _jwtManager.getJwtToken(user.Email, roles);
return tokenJson;
}
else
{
return null; // Return BadRequest and result reason (Failed, lockedout, etc)
}
}
else
{
return null; // User not found, return NotFound }
}
Here's my question - how should I return result from userService to Controller so, that I could respond to API call either with Ok(token) or BadRequest/NotFound with the reason.
If I keep all this LoginAsync code in controller, then it's easy, but I want to use service.
One option I was thinking was to introduce new class, something like:
public class BaseResult
{
public object Data { get; set; }
public long ResponseCode { get; set; }
public string ErrorMessage { get; set; }
}
then always return this class from service, but not fully like that idea either.
thanks!
Here is a working demo you could follow:
Model:
public class UserLoginDto
{
public string Email { get; set; }
public string Password { get; set; }
}
IUserService:
public interface IUserService
{
Task<IActionResult> LoginAsync(UserLoginDto userLogin);
}
UserService:
public class UserService: IUserService
{
private readonly UserManager<IdentityUser> _userManager;
private readonly SignInManager<IdentityUser> _signInManager;
private readonly IJwtManager _jwtManager;
public UserService(
UserManager<IdentityUser> userManager,
SignInManager<IdentityUser> signInManager,
IJwtManager jwtManager)
{
_userManager = userManager;
_signInManager = signInManager;
_jwtManager = jwtManager;
}
public async Task<IActionResult> LoginAsync(UserLoginDto userLogin)
{
var user = await _userManager.FindByEmailAsync(userLogin.Email);
if (user != null)
{
var result = await _signInManager.PasswordSignInAsync(user, userLogin.Password, false, true);
if (result.Succeeded)
{
var roles = await _userManager.GetRolesAsync(user);
var tokenJson = _jwtManager.getJwtToken(user.Email, roles);
return new OkObjectResult(tokenJson);
}
else
{
// Return BadRequest and result reason (Failed, lockedout, etc)
if (result.IsNotAllowed)
{
if (!await _userManager.IsEmailConfirmedAsync(user))
{
// Email isn't confirmed.
return new BadRequestObjectResult("Email isn't confirmed.");
}
if (!await _userManager.IsPhoneNumberConfirmedAsync(user))
{
// Phone Number isn't confirmed.
return new BadRequestObjectResult("Phone Number isn't confirmed.");
}
return new BadRequestObjectResult("Login IsNotAllowed");
}
else if (result.IsLockedOut)
{
// Account is locked out.
return new BadRequestObjectResult("Account is locked out.");
}
else if (result.RequiresTwoFactor)
{
// 2FA required.
return new BadRequestObjectResult("2FA required");
}
else
{
// Password is incorrect.
return new BadRequestObjectResult("Password is incorrect.");
}
}
}
else
{
return new NotFoundObjectResult("Username is incorrect"); // User not found, return NotFound }
}
}
}
Controller:
public class HomeController : Controller
{
private readonly IUserService _userService;
public HomeController(IUserService userService)
{
_userService = userService;
}
[HttpPost]
public async Task<IActionResult> Login([FromBody] UserLoginDto userLogin)
{
var result= await _userService.LoginAsync(userLogin);
return result;
}
}
Startup.cs:
Not sure what is _jwtManager.getJwtToken in your code,so I just guess it is an interface and owns a JwtManager class implemented this interface.And it contains a getJwtToken method which generated the token.
services.AddScoped<IUserService, UserService>();
services.AddScoped<IJwtManager, JwtManager>();

Wiring up validation in MediatR and ASP.NET Core using autofac

I've just started to use MediatR in an asp.net core project and am struggling to wire up validation ...
Here's my controller:
public class PersonController : Controller
{
IMediator mediator;
public PersonController(IMediator mediator)
{
this.mediator = mediator;
}
[HttpPost]
public async Task<ActionResult> Post([FromBody]CreatePerson model)
{
var success = await mediator.Send(model);
if (success)
{
return Ok();
}
else
{
return BadRequest();
}
}
}
... and the CreatePerson command, validation (via FluentValidation) and request handler:
public class CreatePerson : IRequest<bool>
{
public string Title { get; set; }
public string FirstName { get; set; }
public string Surname { get; set; }
}
public class CreatePersonValidator : AbstractValidator<CreatePerson>
{
public CreatePersonValidator()
{
RuleFor(m => m.FirstName).NotEmpty().Length(1, 50);
RuleFor(m => m.Surname).NotEmpty().Length(3, 50);
}
}
public class CreatePersonHandler : IRequestHandler<CreatePerson, bool>
{
public CreatePersonHandler()
{
}
public bool Handle(CreatePerson message)
{
// do some stuff
return true;
}
}
I have this generic validation handler:
public class ValidatorHandler<TRequest, TResponse> : IRequestHandler<TRequest, TResponse> where TRequest : IRequest<TResponse>
{
private readonly IRequestHandler<TRequest, TResponse> inner;
private readonly IValidator<TRequest>[] validators;
public ValidatorHandler(IRequestHandler<TRequest, TResponse> inner, IValidator<TRequest>[] validators)
{
this.inner = inner;
this.validators = validators;
}
public TResponse Handle(TRequest message)
{
var context = new ValidationContext(message);
var failures = validators
.Select(v => v.Validate(context))
.SelectMany(result => result.Errors)
.Where(f => f != null)
.ToList();
if (failures.Any())
throw new ValidationException(failures);
return inner.Handle(message);
}
}
... but I'm struggling to wire the validation up correctly in Startup.ConfigureServices using autofac:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();
var builder = new ContainerBuilder();
builder.Register<SingleInstanceFactory>(ctx =>
{
var c = ctx.Resolve<IComponentContext>();
return t => c.Resolve(t);
});
builder.Register<MultiInstanceFactory>(ctx =>
{
var c = ctx.Resolve<IComponentContext>();
return t => (IEnumerable<object>)c.Resolve(typeof(IEnumerable<>).MakeGenericType(t));
});
builder.RegisterAssemblyTypes(typeof(IMediator).GetTypeInfo().Assembly).AsImplementedInterfaces();
builder.RegisterAssemblyTypes(typeof(CreatePersonHandler).GetTypeInfo().Assembly).AsClosedTypesOf(typeof(IRequestHandler<,>));
builder.RegisterGenericDecorator(typeof(ValidatorHandler<,>), typeof(IRequestHandler<,>), "Validator").InstancePerLifetimeScope();
builder.Populate(services);
var container = builder.Build();
return container.Resolve<IServiceProvider>();
}
When I run the app and POST /api/person
{
"title": "Mr",
"firstName": "Paul",
"surname": ""
}
I get a 200.
CreatePersonHandler.Handle() was called but CreatePersonValidator() is never called.
Am i missing something in Startup.ConfigureServices()?
I suggest that you read the official documentation on how to wire up decorators in Autofac.
Decorators use named services to resolve the decorated services.
For example, in your piece of code:
builder.RegisterGenericDecorator(
typeof(ValidatorHandler<,>),
typeof(IRequestHandler<,>),
"Validator").InstancePerLifetimeScope();
you're instructing Autofac to use ValidationHandler<,> as a decorator to IRequestHandler<,> services that have been registered with the Validator name, which is probably not what you want.
Here's how you could get it working:
// Register the request handlers as named services
builder
.RegisterAssemblyTypes(typeof(CreatePersonHandler).GetTypeInfo().Assembly)
.AsClosedTypesOf(typeof(IRequestHandler<,>))
.Named("BaseImplementation");
// Register the decorators on top of your request handlers
builder.RegisterGenericDecorator(
typeof(ValidatorHandler<,>),
typeof(IRequestHandler<,>),
fromKey: "BaseImplementation").InstancePerLifetimeScope();
I find specifying the name of the fromKey parameter helps in understanding how decorators work with Autofac.

Store and access value obtained during startup from my controller

I am using the opened connect middleware to authenticate with a third party oidc provider and everything is up and running as I would expect. During the token exchange I exchange my auth code for an access token which is successful but I then need to store this bearer token for use later in subsequent requests. The token exchange is done as part of my startup class (by overriding the OpenIdConnectEvents during the OnAuthorizationCodeReceived method) in the asp.net core project and I need to store and access that token in my controllers.
As there is no “session” per se yet, what is the most effective (or recommended way) to store this token value from the startup class and make it accessible in my controllers?
Ive tried to use IMemoryCache but despite putting the value in the cache during this startup phase, when I try and access that cache in my controller, it is always empty.
Is there a better/preferred way of persisting values form the startup class for later use in the lifecycle?
I can see in HttpContext.Authentication.HttpAuthenticationFeature.Handler.Options I have access to all the OpenIdConnectOptions properties and settings for oidc, but nowhere can I see the actual token value that I stored after the token exchange.
I use a similar approach with Auth0 and JWT. I store some app_metadata on the claims server, retrieve, and use these values in my controllers for every request.
Startup.cs Configure
var options = new JwtBearerOptions
{
Audience = AppSettings.Auth0ClientID,
Authority = AppSettings.Auth0Domain
};
app.UseJwtBearerAuthentication(options);
app.UseClaimsTransformation(new ClaimsTransformationOptions
{
Transformer = new Auth0ClaimsTransformer()
});
AdminClaimType
public abstract class AdminClaimType : Enumeration
{
public static readonly AdminClaimType AccountId = new AccountIdType();
public static readonly AdminClaimType ClientId = new ClientIdType();
public static readonly AdminClaimType IsActive = new IsActiveType();
private AdminClaimType(int value, string displayName) : base(value, displayName)
{
}
public abstract string Auth0Key { get; }
public abstract string DefaultValue { get; }
private class AccountIdType : AdminClaimType
{
public AccountIdType() : base(1, "AccountId")
{
}
public override string Auth0Key => "accountId";
public override string DefaultValue => "0";
}
private class ClientIdType : AdminClaimType
{
public ClientIdType() : base(2, "ClientId")
{
}
public override string Auth0Key => "clientId";
public override string DefaultValue => "0";
}
private class IsActiveType : AdminClaimType
{
public IsActiveType() : base(3, "IsActive")
{
}
public override string Auth0Key => "isActive";
public override string DefaultValue => "false";
}
}
Auth0ClaimsTransformer
public class Auth0ClaimsTransformer : IClaimsTransformer
{
private string _accountId = AdminClaimType.AccountId.DefaultValue;
private string _clientId = AdminClaimType.ClientId.DefaultValue;
private string _isActive = AdminClaimType.IsActive.DefaultValue;
public Task<ClaimsPrincipal> TransformAsync(ClaimsTransformationContext context)
{
//TODO: Clean up and simplify AdminClaimTypes Transformer
foreach (var claim in context.Principal.Claims)
{
switch (claim.Type)
{
case "accountId":
_accountId = claim.Value ?? _accountId;
break;
case "clientId":
_clientId = claim.Value ?? _clientId;
break;
case "isActive":
_isActive = claim.Value ?? _isActive;
break;
}
}
((ClaimsIdentity)context.Principal.Identity)
.AddClaims(new Claim[]
{
new Claim(AdminClaimType.AccountId.DisplayName, _accountId),
new Claim(AdminClaimType.ClientId.DisplayName, _clientId),
new Claim(AdminClaimType.IsActive.DisplayName, _isActive)
});
return Task.FromResult(context.Principal);
}
BaseAdminController
//[Authorize]
[ServiceFilter(typeof(ApiExceptionFilter))]
[Route("api/admin/[controller]")]
public class BaseAdminController : Controller
{
private long _accountId;
private long _clientId;
private bool _isActive;
protected long AccountId
{
get
{
var claim = GetClaim(AdminClaimType.AccountId);
if (claim == null)
return 0;
long.TryParse(claim.Value, out _accountId);
return _accountId;
}
}
public long ClientId
{
get
{
var claim = GetClaim(AdminClaimType.ClientId);
if (claim == null)
return 0;
long.TryParse(claim.Value, out _clientId);
return _clientId;
}
}
public bool IsActive
{
get
{
var claim = GetClaim(AdminClaimType.IsActive);
if (claim == null)
return false;
bool.TryParse(claim.Value, out _isActive);
return _isActive;
}
}
public string Auth0UserId
{
get
{
var claim = User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier);
return claim == null ? string.Empty : claim.Value;
}
}
private Claim GetClaim(AdminClaimType claim)
{
return User.Claims.FirstOrDefault(x => x.Type == claim.DisplayName);
}
}
Now in my controller classes that inherit from BaseAdminController I have access to:
AccountId
ClientId
IsActive
Auth0UserId
Anything else I want to add
Hope this helps.
So I figured it out. It is available on HttpContext via the AuthenticationManager:
var idToken = ((AuthenticateInfo)HttpContext.Authentication.GetAuthenticateInfoAsync("Cookies").Result).Properties.Items[".Token.id_token"];
Works a treat :)

Custom RoleProvider failing when AuthorizeAttribute applied with role

I'm having an issue with a custom role provider in ASP.net MVC4. I implemented a very light weight RoleProvider which seems to work fine right up until I change
[Authorize]
public class BlahController:....
}
to
[Authorize(Roles="Administrator")]
public class BlahController:....
}
as soon as I make that change users are no longer authenticated and I get 401 errors. This is odd because my RoleProvider basically returns true for IsUSerInRole and a list containing "Administrator" for GetUserRoles. I had breakpoints in place on every method in my custom RoleProvider and found that none of them were being called.
Next I implemented my own authorize attribute which inherited from AuthorizeAttribute. In this I put in break points so I could see what was going on. It turned out that User.IsInRole(), which is called by the underlying attribute was returning false.
I am confident that the role provider is properly set up. I have this in my config file
<roleManager enabled="true" defaultProvider="SimplicityRoleProvider">
<providers>
<clear />
<add name="SimplicityRoleProvider" type="Simplicity.Authentication.SimplicityRoleProvider" applicationName="Simplicity" />
</providers>
</roleManager>
and checking which role provider is the current one using the method described here: Reference current RoleProvider instance? yields the correct result. However User.IsInRole persists in returning false.
I am using Azure Access Control Services but I don't see how that would be incompatible with a custom role provider.
What can I do to correct the IPrincipal User such that IsInRole returns the value from my custom RoleProvider?
RoleProvider source:
public class SimplicityRoleProvider : RoleProvider
{
private ILog log { get; set; }
public SimplicityRoleProvider()
{
log = LogManager.GetLogger("ff");
}
public override void AddUsersToRoles(string[] usernames, string[] roleNames)
{
log.Warn(usernames);
log.Warn(roleNames);
}
public override string ApplicationName
{
get
{
return "Simplicity";
}
set
{
}
}
public override void CreateRole(string roleName)
{
}
public override bool DeleteRole(string roleName, bool throwOnPopulatedRole)
{
return true;
}
public override string[] FindUsersInRole(string roleName, string usernameToMatch)
{
log.Warn(roleName);
log.Warn(usernameToMatch);
return new string[0];
}
public override string[] GetAllRoles()
{
log.Warn("all roles");
return new string[0];
}
public override string[] GetRolesForUser(string username)
{
log.Warn(username);
return new String[] { "Administrator" };
}
public override string[] GetUsersInRole(string roleName)
{
log.Warn(roleName);
return new string[0];
}
public override bool IsUserInRole(string username, string roleName)
{
log.Warn(username);
log.Warn(roleName);
return true;
}
public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames)
{
}
public override bool RoleExists(string roleName)
{
log.Warn(roleName);
return true;
}
}
It seems that System.Web.Security.Roles.GetRolesForUser(Username) does not get automatically hooked up when you have a custom AuthorizeAttribute and a custom RoleProvider.
So, in your custom AuthorizeAttribute you need to retrieve the list of roles from your data source and then compare them against the roles passed in as parameters to the AuthorizeAttribute.
I have seen in a couple blog posts comments that imply manually comparing roles is not necessary but when we override AuthorizeAttribute it seems that we are suppressing this behavior and need to provide it ourselves.
Anyway, I'll walk through what worked for me. Hopefully it will be of some assistance.
I welcome comments on whether there is a better way to accomplish this.
Note that in my case the AuthorizeAttribute is being applied to an ApiController although I'm not sure that is a relevant piece of information.
public class RequestHashAuthorizeAttribute : AuthorizeAttribute
{
bool requireSsl = true;
public bool RequireSsl
{
get { return requireSsl; }
set { requireSsl = value; }
}
bool requireAuthentication = true;
public bool RequireAuthentication
{
get { return requireAuthentication; }
set { requireAuthentication = value; }
}
public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext ActionContext)
{
if (Authenticate(ActionContext) || !RequireAuthentication)
{
return;
}
else
{
HandleUnauthorizedRequest(ActionContext);
}
}
protected override void HandleUnauthorizedRequest(HttpActionContext ActionContext)
{
var challengeMessage = new System.Net.Http.HttpResponseMessage(HttpStatusCode.Unauthorized);
challengeMessage.Headers.Add("WWW-Authenticate", "Basic");
throw new HttpResponseException(challengeMessage);
}
private bool Authenticate(System.Web.Http.Controllers.HttpActionContext ActionContext)
{
if (RequireSsl && !HttpContext.Current.Request.IsSecureConnection && !HttpContext.Current.Request.IsLocal)
{
//TODO: Return false to require SSL in production - disabled for testing before cert is purchased
//return false;
}
if (!HttpContext.Current.Request.Headers.AllKeys.Contains("Authorization")) return false;
string authHeader = HttpContext.Current.Request.Headers["Authorization"];
IPrincipal principal;
if (TryGetPrincipal(authHeader, out principal))
{
HttpContext.Current.User = principal;
return true;
}
return false;
}
private bool TryGetPrincipal(string AuthHeader, out IPrincipal Principal)
{
var creds = ParseAuthHeader(AuthHeader);
if (creds != null)
{
if (TryGetPrincipal(creds[0], creds[1], creds[2], out Principal)) return true;
}
Principal = null;
return false;
}
private string[] ParseAuthHeader(string authHeader)
{
if (authHeader == null || authHeader.Length == 0 || !authHeader.StartsWith("Basic")) return null;
string base64Credentials = authHeader.Substring(6);
string[] credentials = Encoding.ASCII.GetString(Convert.FromBase64String(base64Credentials)).Split(new char[] { ':' });
if (credentials.Length != 3 || string.IsNullOrEmpty(credentials[0]) || string.IsNullOrEmpty(credentials[1]) || string.IsNullOrEmpty(credentials[2])) return null;
return credentials;
}
private bool TryGetPrincipal(string Username, string ApiKey, string RequestHash, out IPrincipal Principal)
{
Username = Username.Trim();
ApiKey = ApiKey.Trim();
RequestHash = RequestHash.Trim();
//is valid username?
IUserRepository userRepository = new UserRepository();
UserModel user = null;
try
{
user = userRepository.GetUserByUsername(Username);
}
catch (UserNotFoundException)
{
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.Unauthorized));
}
//is valid apikey?
IApiRepository apiRepository = new ApiRepository();
ApiModel api = null;
try
{
api = apiRepository.GetApi(new Guid(ApiKey));
}
catch (ApiNotFoundException)
{
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.Unauthorized));
}
if (user != null)
{
//check if in allowed role
bool isAllowedRole = false;
string[] userRoles = System.Web.Security.Roles.GetRolesForUser(user.Username);
string[] allowedRoles = Roles.Split(','); //Roles is the inherited AuthorizeAttribute.Roles member
foreach(string userRole in userRoles)
{
foreach (string allowedRole in allowedRoles)
{
if (userRole == allowedRole)
{
isAllowedRole = true;
}
}
}
if (!isAllowedRole)
{
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.Unauthorized));
}
Principal = new GenericPrincipal(new GenericIdentity(user.Username), userRoles);
Thread.CurrentPrincipal = Principal;
return true;
}
else
{
Principal = null;
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.Unauthorized));
}
}
}
The custom authorize attribute is governing the following controller:
public class RequestKeyAuthorizeTestController : ApiController
{
[RequestKeyAuthorizeAttribute(Roles="Admin,Bob,Administrator,Clue")]
public HttpResponseMessage Get()
{
return Request.CreateResponse(HttpStatusCode.OK, "RequestKeyAuthorizeTestController");
}
}
In the custom RoleProvider, I have this method:
public override string[] GetRolesForUser(string Username)
{
IRoleRepository roleRepository = new RoleRepository();
RoleModel[] roleModels = roleRepository.GetRolesForUser(Username);
List<string> roles = new List<string>();
foreach (RoleModel roleModel in roleModels)
{
roles.Add(roleModel.Name);
}
return roles.ToArray<string>();
}
So the issue is not how you implement the role provider, but rather how you configure your application to use it. I could not find any issues in your configuration, though. Please make sure this is indeed how you configure your application. This post may help: http://brianlegg.com/post/2011/05/09/Implementing-your-own-RoleProvider-and-MembershipProvider-in-MVC-3.aspx. If you use the default MVC template to create the project, please check the AccountController. According to that post, you may need to do a few modifications to make a custom membership provider work. But that would not affect role providers.
Best Regards,
Ming Xu.
I don't like the custom authorization attribute because I have to remind people to use it. I chose to implement the my own IIdentity/IPrincipal class and wire it up on authorization.
The custom UserIdentity that calls the default RoleProvider:
public class UserIdentity : IIdentity, IPrincipal
{
private readonly IPrincipal _original;
public UserIdentity(IPrincipal original){
_original = original;
}
public string UserId
{
get
{
return _original.Identity.Name;
}
}
public string AuthenticationType
{
get
{
return _original.Identity.AuthenticationType;
}
}
public bool IsAuthenticated
{
get
{
return _original.Identity.IsAuthenticated;
}
}
public string Name
{
get
{
return _original.Identity.Name;
}
}
public IIdentity Identity
{
get
{
return this;
}
}
public bool IsInRole(string role){
return Roles.IsUserInRole(role);
}
}
and added this to global.asax.cs:
void Application_PostAuthenticateRequest(object sender, EventArgs e)
{
if(false == HttpContext.Current.User is UserIdentity){
HttpContext.Current.User = new UserIdentity(HttpContext.Current.User);
}
}
What stimms wrote in his comment: "What I'm seeing is that the IPrincipal doesn't seem to have the correct RoleProvider set" got me looking at the implementation of my custom authentication attribute which inherits from Attribute and IAuthenticationFilter.
using System.Web.Security;
....
protected override async Task<IPrincipal> AuthenticateAsync(string userName, string password, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(userName) || string.IsNullOrWhiteSpace(password))
{
// No user with userName/password exists.
return null;
}
var membershipProvider = Membership.Providers["CustomMembershipProvider"];
if (membershipProvider != null && membershipProvider.ValidateUser(userName, password))
{
ClaimsIdentity identity = new GenericIdentity(userName, "Basic");
return new RolePrincipal("CustomRoleProvider", identity);
}
return null;
}
The key is in returning RolePrincipal, which points to your custom role provider.
Initially I returned new ClaimsPrincipal(identity), which gave me the problem described in the OP.