I'm trying to create a signup/registration page for my Xamarin.Forms app. I've done a fair bit of reading on this and I've learned that authentication is a complicated process so it's best to use a service like OAuth or Azure Active Directory. I even created an account with Azure and connected my app (I think) but the documentation is sparse as far as implementing a registration page. OAuth seemed to have a pretty good tutorial on their site but it was only for Android and iOS, and my app is for Android & UWP. Is there anyone that can show how to implement a registration page in Xamarin.Forms? I don't have much of a user base yet so I'd like to use a service that's free or very cost effective, maybe like a pay per user model.
After connecting my Azure project in Visual Studio this code was auto-generated in a file called Startup.Auth.cs:
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = ConfigurationManager.AppSettings["ida:Audience"]
},
MetadataAddress = ConfigurationManager.AppSettings["ida:MetadataAddress"],
});
}
}
And this file Startup.MobileApp.cs:
public partial class Startup
{
public static void ConfigureMobileApp(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
new MobileAppConfiguration()
.UseDefaultConfiguration()
.ApplyTo(config);
// Use Entity Framework Code First to create database tables based on your DbContext
Database.SetInitializer(new MobileServiceInitializer());
MobileAppSettingsDictionary settings = config.GetMobileAppSettingsProvider().GetMobileAppSettings();
if (string.IsNullOrEmpty(settings.HostName))
{
app.UseAppServiceAuthentication(new AppServiceAuthenticationOptions
{
// This middleware is intended to be used locally for debugging. By default, HostName will
// only have a value when running in an App Service application.
SigningKey = ConfigurationManager.AppSettings["SigningKey"],
ValidAudiences = new[] { ConfigurationManager.AppSettings["ValidAudience"] },
ValidIssuers = new[] { ConfigurationManager.AppSettings["ValidIssuer"] },
TokenHandler = config.GetAppServiceTokenHandler()
});
}
app.UseWebApi(config);
}
}
But I have no idea how to work with these. What I do know is how to make a page with xaml and work with user inputs in the code behind (C#), but once I have user input like a new user name and such how do I connect to the authentication service? Any help would be appreciated, either in the form of detailed instructions or a link to a tutorial. Also I am not looking to use a 3rd party login such as login with Google or login with Facebook.
Related
I have a .NET MVC website where the user is authenticated using AzureAD.
All the controllers are decorated with [Authorize] attribute.
Considering Security, we don't want unauthenticated users to access/download static content of the application like images, .js, .css files.
Can anyone suggest a good approach.
I figured this out and successfully implemented this by using the following:
Add app.UseStageMarker(PipelineStage.Authenticate); at the end
of app.UseOpenIdConnectAuthentication(). Please refer the below code of Startup class.
Add below in Web.configs.
Under <system.webServer> add the below
public partial class Startup
{
public Startup()
{
}
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
CookieManager = new SystemWebChunkingCookieManager(),
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
MetadataAddress = String.Format(Globals.WellKnownMetadata, Globals.TenantId, Globals.DefaultPolicy),
ClientId = Globals.ClientId,
RedirectUri = Globals.RedirectUri,
PostLogoutRedirectUri = Globals.LogoutPostUri,
TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
},
ResponseType = OpenIdConnectResponseType.CodeIdToken,
Scope = OpenIdConnectScope.OpenIdProfile + OpenIdConnectScope.OfflineAccess,
CookieManager = new SystemWebCookieManager()
}
);
app.UseStageMarker(PipelineStage.Authenticate);
}
}
The solution posted by Saca pointed me in the right direction, but adding the JS to every page was not a valid solution for me. There were thousands of HTML files, lots with no common JS file I could tack that ADAL code into. I would have had to find a way to insert that JS on all those pages.
My first solution was simply creating a normal .NET MVC app with the proper auth configured. Then I simply loaded this legacy content via an iFrame. This worked but was limiting for the users.
As Fei Xue mentioned in another comment, the next solution involved scrapping the iFrame but routing all requests for static files through a controller. Using this as a reference for understanding that: https://weblogs.asp.net/jongalloway/asp-net-mvc-routing-intercepting-file-requests-like-index-html-and-what-it-teaches-about-how-routing-works
The above solutions worked. However, eventually this app ended up as an Azure App Service and I simply turned on authentication at the app service level with just the pure html files
[Target netcoreapp3.1]
Hi there! So I have this Web Api that is protected by a middleware of this form in my Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
//other services configuration
services.AddProtectedWebApi(options => { /* config */};
//other services configuration
}
This verifies Jwt Tokens issued by Azure and grants access to the API; it works fine.
At present, I have a front-end angular client website where a user signs in via Azure AD. Angular sends the token to my web API and everything works.
I would now like to use the same webapp to handle query requests from a user without credentials, but with a client certificate that would have been provided in advance. So basically, I'd like to authenticate on my Angular WebSite via Azure OR via a client cert. Angular would then follow up the information to my webapp, which would in turn authenticate the user with the appropriate method.
To be clear, I still want someone to be able to log in without a certificate by using his Azure account.
Is there a simple way to have two authentication options in this case without having to create a separate webapp? I read a bit there : https://learn.microsoft.com/en-us/aspnet/core/security/authentication/certauth?view=aspnetcore-3.1#optional-client-certificates
But it seems it'd only work on the preview of ASP.NET Core 5, which I can't use in my situation.
Hope what follows will help someone!
I eventually found this link : https://learn.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme?view=aspnetcore-3.1
It explains how to implement multiple authorization policies that both have a chance to succeed. Below is the solution I found using IIS after a bit more research:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
//other services configuration
services.Configure<IISOptions>(options =>
{
options.ForwardClientCertificate = true;
});
services.Configure<CertificateForwardingOptions>(options =>
{
options.CertificateHeader = {/*your header present in client request*/};
});
//other services configuration
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.AllowedCertificateTypes =/*Whatever you need*/;
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
if ({/*CertValidationClass*/}.ValidateCertificate(context.ClientCertificate))
{
context.Success();
}
else
{
context.Fail("invalid cert");
}
return Task.CompletedTask;
}
};
});
services.AddProtectedWebApi(options => { /* config */};
//other services configuration
}
{CertValidationClass} being a service or helper class custom made to verify all I have to verify to approve the certificate. Obviously you can add a lot more verifying and actions on your own to this template.
I already had app.UseAuthentication(); app.UseAuthorization(); in my middleware pipeline, no need to change that, but you do have to add app.UseCertificateForwarding(); before these two.
Now I just had to specify above the controller I wanted to protect that I wanted to use both Authorization methods, and just like that, if one fails, it falls back on the other and it works perfectly, I tested by making requests via Insomnia with/without tokens and with/without certficates.
MyApiController.cs
[Authorize(AuthenticationSchemes = AuthSchemes)]
public class MyApiController
{
//Just add the schemes you want used here
private const string AuthSchemes =
CertificateAuthenticationDefaults.AuthenticationScheme; + "," +
JwtBearerDefaults.AuthenticationScheme;
So I have been bashing my head for a while with this problem.
We have one web app that is using IdentityServer4 and AspNetIdentity to authenticate and register users (this is working as intended).
In addition, we have an other API (inside the same solution) that is able to use IdentityServer4 to authenticate users accessing the API.
However, the problem is, that besides authentication we cannot use the API to create new users.
For instance, users should be able to create other users through the web API and not only from the web app, because in our case, users are linked to other users (think of it as multiple profiles).
I am not really familiar with all the configuration services that come up with .Net Core framework and I have tried multiple ways of accessing the user manager of the web app through the API to register my users through classic POST requests but nothing seems to be working. Searching online is tricky because our problem is kind of very specific, that's why I am posting here.
API Startup.cs - ConfigureServices:
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
// base-address of your identityserver
options.Authority = Configuration["IdentityServer:Url"];
// name of the API resource
options.ApiName = Configuration["IdentityServer:APIName"];
options.ApiSecret = Configuration["IdentityServer:APISecret"];
options.EnableCaching = true;
options.CacheDuration = TimeSpan.FromMinutes(10); // that's the default
options.RequireHttpsMetadata = Convert.ToBoolean(Configuration["IdentityServer:RequireHttpsMetadata"]);
});
API Startup.cs - Configure:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseCors("AllowAllOrigins");
app.UseAuthentication();
app.UseMvc();
}
API UsersController.cs - Constructor:
private readonly UserManager<ApplicationUser> _userManager;
private readonly ApplicationDbContext _context;
public UsersController(IUserService service,
ApplicationDbContext context,
UserManager<ApplicationUser> userManager)
{
_service = service;
_userManager = userManager;
_context = context;
}
Now the problem is that when I start the API and try to access the UsersController I get the following error:
System.InvalidOperationException: Unable to resolve service for type 'Microsoft.AspNetCore.Identity.UserManager`1[XXXXX.Data.Models.ApplicationUser]' while attempting to activate 'XXXXXXX.Api.Controllers.UsersController'.
I sincerely hope I can find at least some advice on how to proceed with it.
Please if something is unclear reply and I will be more than happy to add more information or make things clear.
Kind regards,
Marios.
EDIT:
Thanks all for replying. The code snippet provided below by #Vidmantas did the trick.
Due to my limited knowledge of .net core I did a lot of trial and error in the configure services function which, as you can imagine, didn't work. I strongly believe that using .net core is kind of easy (e.g. API), but when it comes to configuring services the complexity (puzzling/confusing mostly) explodes.
As for the architecture, you gave me good ideas for future refactoring. Notes taken.
Marios.
If I understand you correctly, then you are not really supposed to create users through the API - that is why you have Identity Server 4 in place - to provide central authority for authentication for your user base. What you actually need:
a set of API endpoints on the Identity Server 4 side to manage AspNetIdentity
completely new API but one that shares the same database with Identity Server 4 for your AspNetIdentity
have your API share the database for AspNet Identity
If you go with the last option then you probably need something like below to add the:
services.AddDbContext<IdentityContext>(); //make sure it's same database as IdentityServer4
services.AddIdentityCore<ApplicationUser>(options => { });
new IdentityBuilder(typeof(ApplicationUser), typeof(IdentityRole), services)
.AddRoleManager<RoleManager<IdentityRole>>()
.AddSignInManager<SignInManager<ApplicationUser>>()
.AddEntityFrameworkStores<IdentityContext>();
This will give you enough services to use the UserManager and it won't set up any unnecessary authentication schemes.
I would not recommend the last approach due to the separation of concerns - your API should be concerned about providing resources, not creating users and providing resources. First and second approach are alright in my opinion, but I would always lean for clean separate service for AspNetIdentity management.
An example architecture from one of my projects where we implemented such approach:
auth.somedomain.com - IdentityServer4 web app with AspNetIdentity for user authentication.
accounts.somedomain.com - AspNetCore web app with AspNetIdentity (same database as Identity Server 4) for AspNetIdentity user management
webapp1.somedomain.com - a web app where all your front end logic resides (can ofcourse have a backend as well if AspNetCore MVC or something like that)
api1.somedomain.com - a web app purely for API purposes (if you go single app for front end and backend then you can combine the last two)
I have a similar situation as you do.
Identity server with asp .net identity users. (DB contains clients and user data)
API (database contains access to application data) .net Framework
Application .net Framework.
Our use case was that normally new users would be created though the identity server. However we also wanted the ability for the application to invite users. So i could be logged into the application and i wanted to invite my friend. The idea was that the invite would act the same as if a user was creating themselves.
So it would send an email to my friend with a code attached and the user would then be able to supply their password and have an account.
To do this i created a new action on my account controller.
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> Invited([FromQuery] InviteUserRequest request)
{
if (request.Code == null)
{
RedirectToAction(nameof(Login));
}
var user = await _userManager.FindByIdAsync(request.UserId.ToString());
if (user == null)
{
return View("Error");
}
var validateCode = await _userManager.VerifyUserTokenAsync(user, _userManager.Options.Tokens.PasswordResetTokenProvider, "ResetPassword", Uri.UnescapeDataString(request.Code));
if (!validateCode)
{
return RedirectToAction(nameof(Login), new { message = ManageMessageId.PasswordResetFailedError, messageAttachment = "Invalid code." });
}
await _userManager.EnsureEmailConfirmedAsync(user);
await _userManager.EnsureLegacyNotSetAsync(user);
return View(new InvitedViewModel { Error = string.Empty, Email = user.Email, Code = request.Code, UserId = user.Id });
}
When the user accepts the email we add them.
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Invited([FromForm] InvitedViewModel model)
{
if (!ModelState.IsValid)
{
model.Error = "invalid model";
return View(model);
}
if (!model.Password.Equals(model.ConfirmPassword))
{
model.Error = "Passwords must match";
return View(model);
}
if (model.Terms != null && !model.Terms.All(t => t.Accept))
{
return View(model);
}
var user = await _userManager.FindByEmailAsync(model.Email);
if (user == null)
{
// Don't reveal that the user does not exist
return RedirectToAction(nameof(Login), new { message = ManageMessageId.InvitedFailedError, messageAttachment = "User Not invited please invite user again." });
}
var result = await _userManager.ResetPasswordAsync(user, Uri.UnescapeDataString(model.Code), model.Password);
if (result.Succeeded)
{
return Redirect(_settings.Settings.XenaPath);
}
var errors = AddErrors(result);
return RedirectToAction(nameof(Login), new { message = ManageMessageId.InvitedFailedError, messageAttachment = errors });
}
The reason for doing it this way is that only the identity server should be reading and writing to its database. The api and the third party applications should never need to directly change the database controlled by another application. so in this manner the API tells the identity server to invite a user and then the identity server controls everything else itself.
Also by doing it this way it removes your need for having the user manager in your API :)
I would not recommend you to use shared database between different API's.
If you need to extend Identity Server 4 with additional API you can use LocalApiAuthentication for your controllers.
I have a web based app. This app allows users to sign up/in using Google Auth as per this code in Startup.cs
services.AddAuthentication().AddGoogle(googleOptions =>
{
googleOptions.ClientId = Configuration["ClientId"];
googleOptions.ClientSecret = Configuration["CliSecret"];
...
});
This all works nicely with the out-of-the-box Identity system so I can register users.
However, I also want users to be able to 'connect' to other Google services with separate accounts after the sign up in a separate area of the site.
For example, I might want a user to connect their AdWords account.
They will authenticate with Google via a non-Identity flow and the relevant info (token, refresh token etc) will be stored independantly in the db (i.e it won't store a 'User' in the AspNetUSers table).
Can I change the authentication scope in the controller before I make my initial call to google?
It'd be nice to utilize the same Authentication service but with some extra scope in this case. Is that possible?
Alternatively, have another Google section in Startup.cs...maybe like:
services.AddAuthentication().AddGoogle(googleOptions =>
{
googleOptions.ClientId = Configuration["ClientId"];
googleOptions.ClientSecret = Configuration["CliSecret"];
googleOptions.Scope.Add("https://www.googleapis.com/auth/adwords"); //*** THIS IS THE EXTRA SCOPE NEEDED ***
...
});
We had similar problem, our Identity Provider should be able to login users of defferent clients with different Google account
We decided to add multiple Google areas as you suggested. The main point here is that each area (which defines some google account) uses unique cookie scheme.
When we create login URL, we get google account needed for that client, get it's cookie scheme and create correct URL for Google Authenticate button
code example:
public static class AuthenticationBuilderGoogleAdder
{
public static AuthenticationBuilder AddGoogleAuth(this AuthenticationBuilder authenticationBuilder, IServiceCollection services)
{
var serviceProvider = services.BuildServiceProvider();
// create IThirdPartyProvidersProvider realization with GetByProviderCode method
var authThirdPartyProvidersProvider = serviceProvider.GetService<IThirdPartyProvidersProvider>();
var googleProviders = authThirdPartyProvidersProvider.GetByProviderCode("google");
googleProviders.ForEach(p =>
{
authenticationBuilder = authenticationBuilder.AddGoogle(p.CookieScheme, options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.ClientId = p.ClientId;
options.ClientSecret = p.ClientSecret;
});
});
return authenticationBuilder;
}
}
register it as
services.AddAuthentication()
.AddGoogleAuth(services)
We call services.BuildServiceProvider() in order to create another container with services which were already registered in DI, in order to get Google accounts with different cookie schemas from the DB
I am completely new to OWIN authentication, and I must be misunderstanding how everything works, but I can't find this mentioned anywhere.
All I want is to be able to use a central domain for authentication. If someone tries to access apps.domain.com when not authenticated, they will be redirected to accounts.domain.com/login so that all the authentication is separated into it's own domain and application. This was very easy with MVC 4 forms authentication where you can specify a full URL, but doesn't seem to be with OWIN.
In Startup.Auth.cs:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
LoginPath = new PathString("/account/login")
}
It's easy to specify the domain when setting the cookie with the CookieDomain option. However, when you specify the login path to redirect to, it has to be relative to the current application, so how do I go about accomplishing what was so easy in MVC 4 forms authentication?
Without getting too deep into what OWIN authentication is all about, I could not find anything addressing this after a couple hours of searching.
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
LoginPath = new PathString("/account/login"),
LogoutPath = new PathString("/account/logout"),
Provider = new CookieAuthenticationProvider
{
OnApplyRedirect = ApplyRedirect
},
});
}
private static void ApplyRedirect(CookieApplyRedirectContext context)
{
Uri absoluteUri;
if (Uri.TryCreate(context.RedirectUri, UriKind.Absolute, out absoluteUri))
{
var path = PathString.FromUriComponent(absoluteUri);
if (path == context.OwinContext.Request.PathBase + context.Options.LoginPath)
{
context.RedirectUri = "http://accounts.domain.com/login" +
new QueryString(
context.Options.ReturnUrlParameter,
context.Request.Uri.AbsoluteUri);
}
}
context.Response.Redirect(context.RedirectUri);
}
}
If apps.domain.com is the only return URL base possible, you should strongly consider replacing context.Request.Uri.AbsoluteUri with context.Request.PathBase + context.Request.Path + context.Request.QueryString and build an absolute return URL in your authentication server to protect your apps from abusive redirects.
Hope this helps ;)
EDIT: you might ask yourself why I don't directly apply the redirect using the context.RedirectUri property. In fact, ICookieAuthenticationProvider.ApplyRedirect is responsible of multiple redirects, corresponding to the log-in and log-out flows (yep, I know, it breaks the single responsibility principle...). But there's even worse: context.RedirectUri can either represent the authentication endpoint's absolute URL in the beginning of the log-in flow or the final browser's destination (ie. the real relative "return URL") when the cookie is effectively being sent back to the browser... that's why we need to make sure that context.RedirectUri is absolute and corresponds to the registered context.Options.LoginPath.
I am working through the examples for https://github.com/IdentityServer/IdentityServer3 and I have a different answer. In the example at https://www.scottbrady91.com/Identity-Server/Identity-Server-3-Standalone-Implementation-Part-2 they show an MVC app that uses a standalone IdP and cookies authentication. The example hasn't included getting 401 redirects working, but I stumbled on a way.
The basic scheme is to create an action in the AccountController for logging on.
public ActionResult SignIn() {
// set up some bookkeeping and construct the URL to the central auth service
return Redirect(authURL);
}
Now you have a local URL that can be used in the Startup
public class Startup {
public void Configuration(IAppBuilder app) {
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies",
LoginPath = new PathString("/Account/SignIn")
});
}
You also have the added benefit that you can put an action link to the SignIn on the menu bar, for people who want to log on before there is a 401. What we've done here is decoupled the decision of what to do when an unathenticated user asks for a resource from how the authentication is obtained.