ServiceStack: Get email from auth session when authenticating with Google - servicestack

I am authenticating users via GoogleOpenIdOAuthProvider. I need to access the email address of the user that logged in. I have attempted to implement the Using Typed Sessions in ServiceStack code as-is.
So, I created a base class that my service inherits from:
public abstract class AppServiceBase : Service
{
//private CustomUserSession userSession;
protected CustomUserSession UserSession
{
get
{
return base.SessionAs<CustomUserSession>();
}
}
}
public class CustomUserSession : AuthUserSession
{
public string CustomId { get; set; }
}
The service has the [Authenticate] attribute on it. In my AppHost setup, I have configured auth like this:
Plugins.Add(new AuthFeature(() => new CustomUserSession(),
new IAuthProvider[] {
new GoogleOpenIdOAuthProvider(appSettings) //Sign-in with Google OpenId
}));
Once the user has authenticated, the service tries to access the auth session from the base class like this:
var x = base.UserSession.Email;
However, Email is always null. How can I access this value?

You will need to pull the data from the AuthProvider and set the value in the CustomUserSession. An example of this is shown in the SocialBootstrapApi sample
https://github.com/ServiceStack/SocialBootstrapApi/blob/master/src/SocialBootstrapApi/Models/CustomUserSession.cs#L50
Override OnAuthenticated, find the GoogleOpenIdOAuthProvider to get to the email address.
Another example is shown at ServiceStack OAuth - registration instead login

Related

ServiceStack Different Security based on routes

We have a ServiceStack host, in which we have modularised the services. In addition we have a custom authentication solution based on the Basic Authentication. But what we would like to do is have different authentication methods for different services, maybe based on routes? Is this possible?
Secondly, is it possible to assign a common route prefix based on the service? As I said we have modularised our services, and in the AppHost definition we enter the assemblies of the different services, but is it possible to change the route prefix, i.e. Service1 to localhost/api1/servicemethods, Service2 to localhost/api2/servicemethods etc.?
You can limit that a Service should only authenticate with a specific provider by specifying the provider name in the [Authenticate] attribute, e.g:
[Authenticate(AuthenticateService.ApiKeyProvider)]
public class ApiKeyAuthServices : Service
{
public object Any(ApiKeyOnly request) => ...;
}
[Authenticate(AuthenticateService.JwtProvider)]
public class JwtAuthServices : Service
{
public object Any(JwtOnly request) => ...;
}
Otherwise inside your Service you can inspect how the request was authenticated by looking at base.SessionAs<AuthUserSession>().AuthProvider.
For defining dynamic routes have a look at:
Auto Route Generation Strategies
Dynamically adding Route Attributes
Customizing Defined Routes
Although ServiceStack isn't designed to define different sets of Apps within the same AppHost so if that's what you're trying to do I'd recommend instead having different AppHosts and using the Service Gateway for any Service-to-Service communication.
Many thanks for your reply. I must be doing something fundamentally wrong, even though I have registered two custom authproviders, both based on the BasicAuthProvider, using AuthenticateService.GetAuthProviders() returns an empty array.
This is the code I use to register the AuthProviders, and they both allow me to login, so I know they are working.
Plugins.Add(new AuthFeature(() => new CustomUserSession(),
new IAuthProvider[] {
new RMCredentialsAuthProvider(),
new RMKOTAuthProvider()
}));
The code from one of the custom providers is
public class RMKOTAuthProvider : BasicAuthProvider
{
#region Public Constructors
public RMKOTAuthProvider() : base()
{
}
#endregion Public Constructors
#region Public Methods
public override Task<IHttpResult> OnAuthenticatedAsync(IServiceBase authService, IAuthSession session, IAuthTokens tokens, Dictionary<string, string> authInfo, CancellationToken token = default)
{
session.FirstName = session.UserAuthName;
session.Roles = new List<string>
{
"KOT"
};
authService.SaveSessionAsync(session, SessionExpiry);
return base.OnAuthenticatedAsync(authService, session, tokens, authInfo, token);
}
public override Task<bool> TryAuthenticateAsync(IServiceBase authService, string userName, string password, CancellationToken token = default)
{
try
{
if (userName.IsNullOrEmpty() || password.IsNullOrEmpty())
return Task.FromResult(false);
var result = VerifyUser(username, password);
return Task.FromResult(result);
}
catch (InvalidCastException)
{
return Task.FromResult(false);
}
}
#endregion Public Methods
}
Can you please explain what step I am missing such that GetAuthProviders() can list the providers, and I can use the metadata you described earlier.
Many thanks in advance for your help with this.

Razor pages assign roles to users

I am wondering how to create and assign roles in Razor Pages 2.1. application.
I have found how to make them for MVC application (How to create roles in asp.net core and assign them to users and http://hishambinateya.com/role-based-authorization-in-razor-pages), however it does not work for razor pages as I have no IServicesProvider instance.
What I want is just to create admin role and assign it to seeded administrator account. Something similar has been done in this tutorial https://learn.microsoft.com/en-us/aspnet/core/security/authorization/secure-data?view=aspnetcore-2.1, but it seems be sutied for MVC and does not work properly after I applied it to my application. Please help me to understand how to create and seed roles in Razor Pages.
Will be very greatfull for help!
I handle the task next way. First, I used code proposed by Paul Madson in How to create roles in asp.net core and assign them to users. Abovementioned method I have inserted into Startup.cs. It creates administrator role and assigned it to seeded user.
private void CreateRoles(IServiceProvider serviceProvider)
{
var roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
var userManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
Task<IdentityResult> roleResult;
string email = "someone#somewhere.com";
//Check that there is an Administrator role and create if not
Task<bool> hasAdminRole = roleManager.RoleExistsAsync("Administrator");
hasAdminRole.Wait();
if (!hasAdminRole.Result)
{
roleResult = roleManager.CreateAsync(new IdentityRole("Administrator"));
roleResult.Wait();
}
//Check if the admin user exists and create it if not
//Add to the Administrator role
Task<ApplicationUser> testUser = userManager.FindByEmailAsync(email);
testUser.Wait();
if (testUser.Result == null)
{
ApplicationUser administrator = new ApplicationUser
{
Email = email,
UserName = email,
Name = email
};
Task<IdentityResult> newUser = userManager.CreateAsync(administrator, "_AStrongP#ssword!123");
newUser.Wait();
if (newUser.Result.Succeeded)
{
Task<IdentityResult> newUserRole = userManager.AddToRoleAsync(administrator, "Administrator");
newUserRole.Wait();
}
}
}
Then, in the same file in Configure method I add argument (IServiceProvider serviceProvider), so you should have something like Configure(..., IServiceProvider serviceProvider). In the end of Configure method I add
CreateRoles(serviceProvider).
To make this code work create ApplicationUser class somwhere, for example in Data folder:
using Microsoft.AspNetCore.Identity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Sobopedia.Data
{
public class ApplicationUser: IdentityUser
{
public string Name { get; set; }
}
}
Finally, inside ConfigureServices method substitute
services.AddIdentity<ApplicationUser>()
.AddEntityFrameworkStores<SobopediaContext>()
.AddDefaultTokenProviders();
with
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<SobopediaContext>()
.AddDefaultTokenProviders();
As a result, after programm starts in table AspNetRoles you will get a new role, while in table AspNetUsers you will have a new user acuiering administrator role.
Unfortunatelly, after you add the following code
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<SobopediaContext>()
.AddDefaultTokenProviders();
pages Login and Registration stop working. In order to handle this problem you may follow next steps:
Scaffold Identity following (https://learn.microsoft.com/en-us/aspnet/core/security/authentication/scaffold-identity?view=aspnetcore-2.1&tabs=visual-studio).
Then substitute IdentityUser for ApplicationUser in entire solution. Preserv only IdentityUser inheritance in ApplicationUser class.
Remove from Areas/identity/Pages/Account/Register.cs all things related to EmailSernder if you have no its implementation.
In order to check correctness of the roles system you may do as follows. In the end of ConfigureServices method in Startup.cs add this code:
services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdministratorRole", policy => policy.RequireRole("Administrator"));
});
services.AddMvc().AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizeFolder("/Contact","RequireAdministratorRole");
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
If it does not worki then just add [Authorize(Roles = "Administrator")] to Contact Page model, so it will look something like this:
namespace Sobopedia.Pages
{
[Authorize(Roles = "Administrator")]
public class ContactModel : PageModel
{
public string Message { get; set; }
public void OnGet()
{
Message = "Your contact page.";
}
}
}
Now, in order to open Contact page you should be logged in with login someone#somewhere.com and password _AStrongP#ssword!123.

ASP.Net MVC5: How to redirect and show right error message when user does not have a specific role for action

after login user can go to any action but think when action is decorated with authorized attribute and role names are specific there. just refer a sample code.
public class HomeController : Controller
{
[Authorize(Roles = "Admin, HrAdmin")]
public ActionResult PayRoll()
{
return View();
}
}
suppose user Foo has no role like Admin or HRAdmin then what will happen when user foo will try to access PayRoll action ?
in this kind of situation i want to redirect user to my error page where i will show a friendly message to user. please guide me how to do it ?
do i need to write a custom authorized attribute from there i need to check user has those roles are not and then redirect user from there?
I don't know if that's the best way to do it, but here's how I did it:
using System.Web.Mvc;
namespace YourNamespace
{
public class AccessDeniedAuthorizeAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
// Redirect to the login page if necessary
if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
{
filterContext.Result = new RedirectResult(System.Web.Security.FormsAuthentication.LoginUrl + "?returnUrl=" + filterContext.HttpContext.Request.Url);
return;
}
// Redirect to your "access denied" view here
if (filterContext.Result is HttpUnauthorizedResult)
{
filterContext.Result = new RedirectResult("~/Account/Denied");
}
}
}
}
Controller:
public class HomeController : Controller
{
[AccessDeniedAuthorize(Roles = "Admin, HrAdmin")]
public ActionResult PayRoll()
{
return View();
}
}
That's all you have to do if your User has its Roles defined correctly. If you are not using ASP.NET Identity to manage your users and roles, you will need some more code to make this work, in that case this might help you: How can I attach a custom membership provider in my ASP.NET MVC application?.

ServiceStack Custom Credentials Auth with DB Stored Api Keys

Right now, we're authenticating our users with this:
public class WindowsAuthProvider : CredentialsAuthProvider
{
public override bool TryAuthenticate(IServiceBase authService, string userName, string password)
{
using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, "OurDomain"))
{
// TODO make sure user record exists in custom DB tables as well
return pc.ValidateCredentials(userName, password);
}
}
public override IHttpResult OnAuthenticated(IServiceBase authService, IAuthSession session, IAuthTokens tokens, Dictionary<string, string> authInfo)
{
return base.OnAuthenticated(authService, session, tokens, authInfo);
}
}
Which works great when using the JsonServiceClient.
We have some legacy code written in Visual FoxPro which wants to call some of the authenticated functions in ServiceStack... to accommodate this, we'd like to also allow Api Keys. We want the API Keys to be stored in SQL Server to avoid issues if the process stops / restarts. So, the client would authenticate with domain credentials, then generate an API key for subsequent calls which would be stored in the database (ideally just using the table servicestack can create (dbo.ApiKey).
If we were to set this per the docs:
container.Register<IAuthRepository>(c => new OrmLiteAuthRepository(dbFactory));
We get an error on the OnAuthenticated function above telling us we should call Init()... like its trying to also create the user tables. So I'm not sure how to allow DB stored API Keys, along with custom authentication that relies on both active directory as well as our custom tables for users and roles.
Instead of inheriting from CredentialsAuthProvider, maybe its better to register a custom IUserAuthRepository and IManageRoles?
The API Key AuthProvider needs to be registered in your AuthFeature, e.g:
Plugins.Add(new AuthFeature(...,
new IAuthProvider[] {
new ApiKeyAuthProvider(AppSettings),
new WindowsAuthProvider(AppSettings),
//...
}));
Which requires a IAuthRepository like you're doing:
container.Register<IAuthRepository>(c =>
new OrmLiteAuthRepository(dbFactory));
Any AuthProvider that requires creating a back-end tables or other schema requires that its schema is initialized on Startup which you can do with:
container.Resolve<IAuthRepository>().InitSchema();
It's safe to always call InitSchema() as it only creates missing tables or is otherwise ignored for AuthRepositories that don't require creating a schema.
An issue you're running into is that you've registered an IAuthRepository and are inheriting a CredentialsAuthProvider which you don't want to use it in so you can't call CredentialsAuthProvider.OnAuthenticated() since it will save the User Auth info to the repository if it exists.
So you'll need to provide a custom implement without calling base.OnAuthenticated(), e.g:
public class WindowsAuthProvider : CredentialsAuthProvider
{
public override bool TryAuthenticate(IServiceBase authService, string userName, string password)
{
using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, "OurDomain"))
{
// TODO make sure user record exists in custom DB tables as well
return pc.ValidateCredentials(userName, password);
}
}
public override IHttpResult OnAuthenticated(IServiceBase authService, IAuthSession session, IAuthTokens tokens, Dictionary<string, string> authInfo)
{
try
{
session.IsAuthenticated = true;
session.OnAuthenticated(authService, session, tokens, authInfo);
AuthEvents.OnAuthenticated(authService.Request, session, authService, tokens, authInfo);
}
finally
{
this.SaveSession(authService, session, SessionExpiry);
}
return null;
}
}

Cannot SignOut using Asp.Net Identity with Owin

I'm unable to log off the user in an MVC 5 app using ASP.NET Identity with Owin (latest versions). Login works great...but I can't log the user off without opening the browser settings to delete the cookie.
When the LogOff action runs, the browser redirects to the designated page which has the [Authorize] attribute. It should be rejected at that point and redirected to the Login page.
Note that if I manually delete the cookies, it will redirect correctly when attempting to open the [Authorize] page, so the redirect action for unauthenticated users is working correctly.
I see a lot of similar questions and have tried the solutions, but nothing is working so far.
I changed:
AuthenticationManager.SignOut();
To:
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
as suggested in previous answers, but it didn't change the behavior.
Login works fine. I notice after attempting to LogOff, there are two cookies with the same name instead of just one. One cookie is empty, and one is not.
Here's my LogOff method:
[HttpPost]
[AllowAnonymous]
public ActionResult LogOff()
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
//Clear the principal to ensure the user does not retain any authentication
HttpContext.User = new GenericPrincipal(new GenericIdentity(string.Empty), null);
// Redirect to a controller/action that requires authentication to ensure a redirect takes place
// this clears the Request.IsAuthenticated flag since this triggers a new request
return RedirectToLocal(String.Empty);
}
And my OwinStartup class:
public class OwinStartup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
// For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
public void ConfigureAuth(IAppBuilder app)
{
// UserManagerFactory = () => new UserManager<IdentityUser>(new UserStore<IdentityUser>(new TenantDbContext()));
// Configure the db context, user manager and signin manager to use a single instance per request
app.CreatePerOwinContext(TenantDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
// Configure the sign in cookie
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
CookieSecure = CookieSecureOption.Always,
Provider = new CookieAuthProvider
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
}
public class CookieAuthProvider : CookieAuthenticationProvider
{
public override void ResponseSignIn(CookieResponseSignInContext context)
{
context.CookieOptions.Domain = context.Request.Uri.Host;
base.ResponseSignIn(context);
}
}
}
And here's my AuthenticationManager:
private IAuthenticationManager AuthenticationManager
{
get
{
return HttpContext.GetOwinContext().Authentication;
}
}

Resources