I using ServiceStack authentication with a custom session object. I've got everything set up with different authentication providers and everything is working fine.
Now a want to store some information in the session before the user is authenticated (Think shopping cart). But we loose that information when the user logs in later. Looking at the code in the documentation this makes sense:
Plugins.Add(new AuthFeature(() => new AuthUserSession(),
new IAuthProvider[] {
new BasicAuthProvider(), //Sign-in with Basic Auth
new CredentialsAuthProvider(), //HTML Form post of UserName/Password credentials
}));
The authentication removes the existing session whenever a user logs in. This makes sense when the old login is a valid user, you want to make sure it's fully logged out. However when the current session isn't authenticated there doesn't seem to be much reason to do so.
I've been looking at a custom session factory, but that doesn't help me because as () => new AuthUserSession() shows, there isn't any context to use when creating the new session. Without a way to get the old session there I've got no way to copy any information.
I can work around it by overriding AuthProvider.Authenticate() and grab the required information before calling base. But that means doing so in every authentication provider we use and the ones we might use in the future. That doesn't really feel like the correct solution.
Is there a cleaner way to carry information across the authentication? Preferably something which works regardless of the AuthProvider used.
Whilst the Typed Sessions are re-created after authenticating, the Permanent and Temporary Session Ids themselves remain the same which lets you use ServiceStack's dynamic SessionBag to store information about a user which you can set in your Services with:
public class UnAuthInfo
{
public string CustomInfo { get; set; }
}
public class MyServices : Service
{
public object Any(Request request)
{
var unAuthInfo = SessionBag.Get<UnAuthInfo>(typeof(UnAuthInfo).Name)
?? new UnAuthInfo();
unAuthInfo.CustomInfo = request.CustomInfo;
SessionBag.Set(typeof(UnAuthInfo).Name, unAuthInfo);
}
}
You can then access the dynamic Session Bag in your Custom AuthUserSession Session Events with:
public class CustomUserSession : AuthUserSession
{
[DataMember]
public string CustomInfo { get; set; }
public override void OnAuthenticated(IServiceBase service, IAuthSession session,
IAuthTokens tokens, Dictionary<string, string> authInfo)
{
var sessionBag = new SessionFactory(service.GetCacheClient())
.GetOrCreateSession();
var unAuthInfo = sessionBag.Get<UnAuthInfo>(typeof(UnAuthInfo).Name);
if (unAuthInfo != null)
this.CustomInfo = unAuthInfo.CustomInfo;
}
}
New Session API's in v4.0.32+
Accessing the Session bag will be a little nicer in next v4.0.32+ of ServiceStack with the new GetSessionBag() and convenience ISession Get/Set extension methods which will let you rewrite the above like:
public object Any(Request request)
{
var unAuthInfo = SessionBag.Get<UnAuthInfo>() ?? new UnAuthInfo();
unAuthInfo.CustomInfo = request.CustomInfo;
SessionBag.Set(unAuthInfo);
}
//...
public override void OnAuthenticated(IServiceBase service, IAuthSession session,
IAuthTokens tokens, Dictionary<string, string> authInfo)
{
var unAuthInfo = service.GetSessionBag().Get<UnAuthInfo>();
if (unAuthInfo != null)
this.CustomInfo = unAuthInfo.CustomInfo;
}
Related
A few days ago, half of our Azure hosted ASP.NET Web API (.NET Framework) started playing up. It's been running fine for a number of months, then all of a sudden, customers couldn't log into one of our front end sites.
We have three applications that connect to the Web API. Two of them connect through the controllers in the top level Controllers folder, and one of them connects through the controllers inside an Area. It's this Area that is causing us pain.
public class MyAppAreaRegistration : AreaRegistration
{
public override string AreaName => "MyApp";
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute("MyAppApi", "myapp/api/{controller}/{action}");
}
}
Both parts of the Web API use the same code to generate Jwt Security Tokens, and unauthorized methods in both areas are okay.
private JwtSecurityToken GenerateSecurityToken(int userId, string username, int customerId)
{
var signingKey = GetSigningKey();
var audience = GetSiteUrl(); // Must match the url of the site
var issuer = GetSiteUrl(); // Must match the url of the site
var lifeTime = TimeSpan.FromHours(24);
var now = DateTime.Now;
var expiry = now.Add(lifeTime);
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()),
new Claim(JwtRegisteredClaimNames.UniqueName, username),
new Claim(JwtRegisteredClaimNames.Prn, customerId.ToString()),
new Claim(JwtRegisteredClaimNames.Iat, now.Ticks.ToString()),
new Claim(JwtRegisteredClaimNames.Exp, expiry.Ticks.ToString())
};
var token = AppServiceLoginHandler.CreateToken(claims, signingKey, audience, issuer, lifeTime);
return token;
}
After much effort trying to work out what could have changed, we implemented a CustomAuthorizeAttribute and applied it to a controller inside the Area and a controller outside the Area. The one outside the Area appears to have the actionContext.ControllerContext.RequestContext.Principal.Identity set correctly, but the one inside the area is empty. Not null. Just empty. We logged both the actionContext.ControllerContext.RequestContext.Principal as well as the Thread.CurrentPrincipal, but both are empty.
I can't seem to work out what is responsible for managing this data, and why it now refuses to do it for controllers with the area.
I'm able to access the token and extract the claims successfully.
All help will be appreciated.
Cheers
I'm trying to achieve the following:
Have an unauthenticated user navigate to a web page, where a SignalR (core) client will connect to a hub (say Notifications hub).
Have the user perform an action and, when the operation is completed on the server, use SignalR to notify him of the completion.
The problem: when a user is logged, I find his SignalR connectionId by a connectionId-username map that is saved in memory. Then I do:
hub.SendConnectionAsync(connectionId, "Message", data);
If the user is not authenticated, I came up with using SessionId, and the map I save in memory is something that gives me a ConnectionId given a SessionId. The code snippet I use on the HubLifetimeManager is something like:
public override async Task OnConnectedAsync(HubConnectionContext connection)
{
await _wrappedHubLifetimeManager.OnConnectedAsync(connection);
_connections.Add(connection);
string userId;
if (connection.User.Identity.IsAuthenticated)
{
userId = connection.User.Identity.Name;
}
else
{
var httpContext = connection.GetHttpContext();
if (httpContext == null)
{
throw new Exception("HttpContext can't be null in a SignalR Hub!!");
}
var sessionId = httpContext.Session.Id;
userId = $"{Constants.AnonymousUserIdentifierPrefix}{sessionId}";
}
await _userTracker.AddUser(connection, new UserDetails(connection.ConnectionId, userId));
}
Problem: if my page is opened in an iframe, httpContext.Session.Id is the empty string, it looks like the cookies of my page opened in the iframe (among which is the Session cookie), are not added to the http requests performed by the javascript code executed inside the iframe...
More generally, how do you identify a user if he's not authenticated? Is there anything in the HttpRequest that you can use as a unique id, like machine name or ip?
If you want to identify an anonymous user you could use a custom http header generated on frontend. It can be accessed with IHttpContextAccessor in combination with custom IUserIdProvider:
public class CustomUserIdProvider : IUserIdProvider
{
private readonly IHttpContextAccessor _httpContextAccessor;
public CustomUserIdProvider(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public string GetUserId(HubConnectionContext connection)
{
if (connection.User.Identity.IsAuthenticated)
{
return connection.User.Identity.Name;
}
var username = _httpContextAccessor.HttpContext?.Request.Headers["username"];
if (username.HasValue && !StringValues.IsNullOrEmpty(username.Value))
{
return username.Value;
}
return Guid.NewGuid().ToString();
}
}
Remember that in .NET Core you need to explicitly add IHttpContextAccessor to the DI container:
services.AddHttpContextAccessor();
services.AddSingleton<IUserIdProvider, CustomUserIdProvider>();
services.AddSignalR();
Then you can use the generated identifier in hub method like this:
public override async Task OnConnectedAsync(HubConnectionContext connection)
{
await _wrappedHubLifetimeManager.OnConnectedAsync(connection);
_connections.Add(connection);
string userId = connection.UserIdentifier;
await _userTracker.AddUser(connection, new UserDetails(connection.ConnectionId, userId));
}
Source: https://dejanstojanovic.net/aspnet/2020/march/custom-signalr-hub-authorization-in-aspnet-core/
I am trying to use IdentityServer4 in a new project. I have seen in the PluralSight video 'Understanding ASP.NET Core Security' that IdentityServer4 can be used with claims based security to secure a web API. I have setup my IdentityServer4 as a separate project/solution.
I have also seen that you can add an IProfileService to add custom claims to the token which is returned by IdentityServer4.
One plan is to add new claims to users to grant them access to different parts of the api. However I can't figure out how to manage the claims of the users on the IdentityServer from the api project. I assume I should be making calls to IdentotyServer4 to add and remove a users claims?
Additionally is this a good approach in general, as I'm not sure allowing clients to add claims to the IdentityServer for their own internal security purposes makes sense - and could cause conflicts (eg multiple clients using the 'role' claim with value 'admin'). Perhaps I should be handling the security locally inside the api project and then just using the 'sub' claim to look them up?
Does anyone have a good approach for this?
Thanks
Old question but still relevant. As leastprivilege said in the comments
claims are about identity - not permissions
This rings true, but identity can also entail what type of user it is (Admin, User, Manager, etc) which can be used to determine permissions in your API. Perhaps setting up user roles with specific permissions? Essentially you could also split up Roles between clients as well for more control if CLIENT1-Admin should not have same permissions as CLIENT2-Admin.
So pass your Roles as a claim in your IProfileService.
public class ProfileService : IProfileService
{
private readonly Services.IUserService _userService;
public ProfileService(Services.IUserService userService)
{
_userService = userService;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
try
{
switch (context.Client.ClientId)
{
//setup profile data for each different client
case "CLIENT1":
{
//sub is your userId.
var userId = context.Subject.Claims.FirstOrDefault(x => x.Type == "sub");
if (!string.IsNullOrEmpty(userId?.Value) && long.Parse(userId.Value) > 0)
{
//get the actual user object from the database
var user = await _userService.GetUserAsync(long.Parse(userId.Value));
// issue the claims for the user
if (user != null)
{
var claims = GetCLIENT1Claims(user);
//add the claims
context.IssuedClaims = claims.Where(x => context.RequestedClaimTypes.Contains(x.Type)).ToList();
}
}
}
break;
case "CLIENT2":
{
//...
}
}
}
catch (Exception ex)
{
//log your exceptions
}
}
// Gets all significant user claims that should be included
private static Claim[] GetCLIENT1Claims(User user)
{
var claims = new List<Claim>
{
new Claim("user_id", user.UserId.ToString() ?? ""),
new Claim(JwtClaimTypes.Name, user.Name),
new Claim(JwtClaimTypes.Email, user.Email ?? ""),
new Claim("some_other_claim", user.Some_Other_Info ?? "")
};
//----- THIS IS WHERE ROLES ARE ADDED ------
//user roles which are just string[] = { "CLIENT1-Admin", "CLIENT1-User", .. }
foreach (string role in user.Roles)
claims.Add(new Claim(JwtClaimTypes.Role, role));
return claims.ToArray();
}
}
Then add [Authorize] attribute to you controllers for your specific permissions. This only allow specific roles to access them, hence setting up your own permissions.
[Authorize(Roles = "CLIENT1-Admin, CLIENT2-Admin, ...")]
public class ValuesController : Controller
{
//...
}
These claims above can also be passed on authentication for example if you are using a ResourceOwner setup with custom ResourceOwnerPasswordValidator. You can just pass the claims the same way in the Validation method like so.
context.Result = new GrantValidationResult(
subject: user.UserId.ToString(),
authenticationMethod: "custom",
claims: GetClaims(user));
So like leastprivilege said, you dont want to use IdentityServer for setting up permissions and passing that as claims (like who can edit what record), as they are way too specific and clutter the token, however setting up Roles that -
grant them access to different parts of the api.
This is perfectly fine with User roles.
Hope this helps.
I am pretty new to this.. so any help would be greatly appreciated.
I have a WebApi service that uses OAUTH token and refresh token authentication.
All works well at the moment:
step1: I send in the user and password and it generates an authentication token and a refresh token. The refresh token is saved in the DB.
step2. I can now use the refresh token and i receive the authentication token and a new refresh token. I want a way to use the same refresh token i sent and not reuse a new one.
This is my code for the refresh token:
public class SimpleRefreshTokenProvider : IAuthenticationTokenProvider
{
public async Task CreateAsync(AuthenticationTokenCreateContext context)
{
RefreshTokensRepository _repo = new RefreshTokensRepository();
var clientid = context.Ticket.Properties.Dictionary["as:client_id"];
//HERE I regenerate the token, but I have no idea how to retrieve the already sent one.
var refreshTokenId = Guid.NewGuid().ToString("n");
//saving in BD:
var refreshTokenLifeTime = context.OwinContext.Get<string>("as:clientRefreshTokenLifeTime");
var token = new RefreshTokens()
{
Id = Helper.GetHash(refreshTokenId),
ClientId = clientid,
Subject = context.Ticket.Identity.Name,
IssuedUtc = DateTime.UtcNow,
ExpiresUtc = DateTime.UtcNow.AddMinutes(Convert.ToDouble(refreshTokenLifeTime))
};
context.Ticket.Properties.IssuedUtc = DateTime.UtcNow;
context.Ticket.Properties.ExpiresUtc = DateTime.UtcNow.AddMinutes(Convert.ToDouble(refreshTokenLifeTime));
token.ProtectedTicket = context.SerializeTicket();
var result = _repo.Add(token);
if(!string.IsNullOrEmpty(result))
context.SetToken(refreshTokenId);
}
public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
{
var allowedOrigin = context.OwinContext.Get<string>("as:clientAllowedOrigin");
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
string hashedTokenId = Helper.GetHash(context.Token);
RefreshTokensRepository _repo = new RefreshTokensRepository();
var refreshToken = _repo.FindById(hashedTokenId);
if (refreshToken != null)
{
//Get protectedTicket from refreshToken class
context.DeserializeTicket(refreshToken.ProtectedTicket);
_repo.Remove(hashedTokenId);
}
}
void IAuthenticationTokenProvider.Create(AuthenticationTokenCreateContext context)
{
throw new NotImplementedException();
}
void IAuthenticationTokenProvider.Receive(AuthenticationTokenReceiveContext context)
{
throw new NotImplementedException();
}
}
My code is based on this samples:
http://bitoftech.net/2014/07/16/enable-oauth-refresh-tokens-angularjs-app-using-asp-net-web-api-2-owin/
I would like to use the same sent refresh token, but I have no idea how to use the already sent one in this context.
Any ideas?
Disclaimer: I don't condone reusing refresh tokens.
However, this does provide a good opportunity for everyone to improve knowledge of how this process works and there could be a good reason for reusing past refresh tokens in certain scenarios. I'm basing my answer upon:
Question: "I want a way to use the same refresh token i sent and not reuse a new one."
Code comment, "//HERE I regenerate the token, but I have no idea how to retrieve the already sent one."
PseudoCode Steps:
Store a user identifier as a property in AuthenticationProperties in the GrantResourceOwnerCredentials() method. From the sample code, it looks like you may already be doing this with "userName":
var props = new AuthenticationProperties(new Dictionary<string, string>
{
{
"as:client_id", (context.ClientId == null) ? string.Empty : context.ClientId
},{
"userName", context.UserName
}
});
Retrieve the user identifier in the CreateAsync() method of your IAuthenticationTokenProvider implementation (e.g. "SimpleRefreshTokenProvider" in your case). This would look something like:
public async Task CreateAsync(AuthenticationTokenCreateContext context)
{
var userName = context.Ticket.Properties.Dictionary["userName"];
...
Still in the CreateAsync() method use the user identifier to lookup the existing refresh token. This would look something like:
var existingRefreshToken = await _repo.FindRefreshTokenByUserNameAsync(userName);
Note: You would need to write the above method into your AuthRepository class from the example code. The "FindRefreshTokenByUserNameAsync(userName) implementation might include something like this if you're using Entity Framework and have a "RefreshToken" table that is being used to persist the granted refresh token:
var existingToken = RefreshToken.Where(r => r.UserName == userName).SingleOrDefault();
At this point, you have the existing token and should be able to re-use that refresh token value instead of Guid.NewGuid():
var refreshTokenId = existingToken.Token;
Taking a look at the tutorial's example code, however, indicates that a HashAlgorithm is being used to store the refresh token's value. That could complicate things a bit for you as storing a hash value is better security, but the process of hashing here is meant to be one-way.
If you really want to reuse the original token value when all you have persisted is the hashed token, would need to implement code that captures the non-hashed token value in the ReceiveAsync() method. It would have to temporarily persist the non-hashed value long enough for you to use it in the CreateAsync() method. In other words, you would have to save/persist the "context.Token" in ReceiveAsync(), associate it with your userName (from context.Ticket.Properties.Dictionary["userName"]), and use it later in the CreateAsync() method. It's hacky and I don't like it, but you would do it around this line of code in ReceiveAsync():
string hashedTokenId = Helper.GetHash(context.Token);
I currently have an IIS hosted application that I would like to switch over to use the self-hosted method.
But I'm having difficulty accessing the session so I can retrieve the current users username.
This is the code I used when hosting under IIS which worked perfectly:
/// <summary>
/// A basic wrapper for the service stack session, to allow access to it lower down in the DAL layer without tying us to servicestack.
/// </summary>
public class ServiceStackAuthTokenService : IAuthTokenService
{
/// <summary>
/// GetCurrentAuthToken.
/// </summary>
/// <returns>A string representing the users auth name.</returns>
public string GetCurrentAuthToken()
{
// Grab the current request.
var req = HttpContext.Current.Request.ToRequest();
var res = HttpContext.Current.Response.ToResponse();
// Fetch the authentication service.
var authService = EndpointHost.AppHost.TryResolve<AuthService>();
authService.RequestContext = new HttpRequestContext(req, res, null);
// Grab the session.
var session = authService.GetSession(false);
// Return the username.
return session.UserName;
}
public string UserPropertyName
{
get { return "UserName"; }
}
}
This is added to the app host with the following code::
container.RegisterAutoWiredAs<ServiceStackAuthTokenService, IAuthTokenService>()
When running self-hosted the HttpContext.Current is null, how do I access the request under a self-hosted application?
Thanks!
Update
Additional things I have tried:
as per an post here: https://groups.google.com/forum/#!msg/servicestack/jnX8UwRWN8A/_XWzTGbnuHgJ
It was suggested to use:
container.Register>(c => AuthService.CurrentSessionFactory);
This just returns a newed IAuthSession.
What the user in that post is doing is exactly what I'm trying to achieve.
In the last post Mythz says:
Just to be clear, in order to form the Session Key that references the Users session you need either the ss-id or ss-pid cookies (as determined by ss-opts).
You can get cookies off the IHttpRequest object or otherwise in ASP.NET the HttpContext.Current.Request singleton, so whatever IAuthUserSession factory you inject needs to take something that can give it the cookies, i.e. either an IRequestContext, IHttpRequest, IService, etc.
But I still cant see a way to access the IHttpRequest.
For ServiceStack 3, you can share request data via the HostContext.Instance.Items Dictionary. For ServiceStack 4, you should use the HostContext.RequestContext.Items Dictionary.
For example, add a request filter in your app host configuration to save the value:
// Put the session into the hostcontext.
RequestFilters.Add((req, res, requestDto) =>
{
HostContext.Instance.Items.Add("Session", req.GetSession());
});
Then in your authentication token class pull it back out:
public string GetCurrentAuthToken()
{
var session = HostContext.Instance.Items["Session"] as AuthUserSession;
if (session != null)
{
return session.UserName;
}
throw new Exception("No attached session found.");
}