OAuth2 Authentication Providers in ASP.NET Web Forms Applications
- 11 minutes to read
This topic demonstrates how to extend your ASP.NET Web Forms application with OAuth authentication providers. Users can sign in to the application with their Microsoft accounts. Refer to the following article to learn how to add more authentication providers: OWIN OAuth 2.0 Authorization Server.
Note
Third-party API and settings of OAuth2 services (for example, Microsoft) often change. We recommend that you refer to the official OAuth2 provider documentation for the latest information and instructions.
Prepare Your Solution
Register developer accounts in the services you want to use in your application. For example, you can register a developer account in Microsoft Azure as described in the following article: Tutorial: Add sign-in to Microsoft to an ASP.NET web app.
In the Web Forms application’s Web.config file, ensure that the authentication mode is set to
Forms
and give unauthenticated users access to the desired OAuth2 providers’ callback routes.File: MySolution.Web\Web.config.
<?xml version="1.0" encoding="utf-8"?> <!-- ... --> <system.web> <!-- ... --> <authentication mode="Forms"> <forms name="Login" loginUrl="Login.aspx" timeout="1" defaultUrl="~/" /> </authentication> <authorization> <deny users="?" /> <allow users="*" /> </authorization> <!-- ... --> </system.web> <!-- ... --> <location path="signin-microsoft"> <system.web> <authorization> <allow users="?" /> </authorization> </system.web> </location>
When you register your application in Azure Portal, add the “/signin-microsoft“ string to Redirect URIs:
Make sure your projects target at least .NET Framework version 4.7.2.
Invoke the Package Manager Console and add the following NuGet packages to your projects:
MySolution.Web project
Install-Package Microsoft.AspNet.Identity.Core -Version 2.2.3 Install-Package Microsoft.AspNet.Identity.Owin -Version 2.2.3 Install-Package Microsoft.Owin -Version 4.2.2 Install-Package Microsoft.Owin.Cors -Version 4.2.2 Install-Package Microsoft.Owin.Security -Version 4.2.2 Install-Package Microsoft.Owin.Security.Cookies -Version 4.2.2 Install-Package Microsoft.Owin.Host.SystemWeb -Version 4.2.2 Install-Package Microsoft.Owin.Security.MicrosoftAccount -Version 4.2.2 Install-Package Microsoft.Owin.Security.OpenIdConnect -Version 4.2.2 Install-Package Microsoft.Identity.Web -Version 1.25.3
MySolution.Module.Web
Install-Package Microsoft.AspNet.Cors -Version 5.2.9 Install-Package Microsoft.Owin -Version 4.2.2 Install-Package Microsoft.Owin.Host.SystemWeb -Version 4.2.2 Install-Package Microsoft.Owin.Security -Version 4.2.2 Install-Package Microsoft.Owin.Security.OpenIdConnect -Version 4.2.2
Configure the OWIN Service
In the application project, create the Startup
class and configure the OWIN service within it. Use the OwinStartup attribute to mark this class as an OWIN startup class:
File: MySolution.Web\Startup.cs(.vb).
using Microsoft.Owin;
using Microsoft.Owin.Extensions;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using System;
using System.Configuration;
using System.Security.Claims;
using System.Threading.Tasks;
[assembly: OwinStartup(typeof(MySolution.Web.Startup))]
namespace MySolution.Web {
public class Startup {
private static string clientId = ConfigurationManager.AppSettings["ClientId"];
private static string tenantId = ConfigurationManager.AppSettings["Tenant"];
private static string authority = string.Format(EnsureTrailingSlash(ConfigurationManager.AppSettings["Authority"]), tenantId);
private static string postLogoutRedirectUri = ConfigurationManager.AppSettings["redirectUri"];
public void Configuration(IAppBuilder app) {
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions {
ClientId = clientId,
Authority = authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications() {
AuthenticationFailed = (context) => {
return Task.FromResult(0);
},
SecurityTokenValidated = (context) => {
string name = context.AuthenticationTicket.Identity.FindFirst("preferred_username").Value;
context.AuthenticationTicket.Identity.AddClaim(new Claim(ClaimTypes.Name, name, string.Empty));
return Task.FromResult(0);
}
}
});
app.UseStageMarker(PipelineStage.Authenticate);
}
private static string EnsureTrailingSlash(string value) {
if(value == null) {
value = string.Empty;
}
if(!value.EndsWith("/", StringComparison.Ordinal)) {
return value + "/";
}
return value;
}
}
}
Configure the Logon Page
Add the new LogonTemplateContent template to the application project. To do this, right-click the MySolution.Web project, click Add DevExpress Item | New Item…, and choose Logon Template Content in the invoked Template Gallery:
Edit the autogenerated code as demonstrated below:
File: MySolution.Web\LogonTemplateContent1.ascx.
<div class="LogonTemplate"> <%--...--%> <div style="top: 25%; width: 100%; position: absolute"> <table class="LogonMainTable LogonContentWidth"> <%--...--%> <tr> <td> <table class="LogonContent LogonContentWidth"> <%--...--%> <tr> <td> <table id="UseExisting" class="StaticText width100" style="margin: 50px 0 20px;"> <tr> <td style="min-width: 130px;">or use existing</td> <td class="width100" style="padding-top: 7px;"><hr></td> </tr> </table> <xaf:XafUpdatePanel ID="XafUpdatePanelOAuth" runat="server" CssClass="UPOAuth right"> <xaf:ActionContainerHolder ID="OAuthActions" CssClass="UPOAuthACH" runat="server" Orientation="Horizontal" ContainerStyle="Buttons"> <Menu ID="OAuthMenu" ClientInstanceName="UPOAuthMenu" HorizontalAlign="Left" Width="100%" ItemAutoWidth="False" /> <ActionContainers> <xaf:WebActionContainer ContainerId="OAuthActions" /> </ActionContainers> </xaf:ActionContainerHolder> </xaf:XafUpdatePanel> </td> </tr> </table> </td> </tr> </table> </div> </div>
Register the LogonTemplateContent1.ascx template in the
Session\_Start
method:File: MySolution.Web\Global.asax.cs(.vb).
public class Global : System.Web.HttpApplication { // ... protected void Session_Start(Object sender, EventArgs e) { // ... WebApplication.Instance.Settings.LogonTemplateContentPath = "LogonTemplateContent1.ascx"; // ... WebApplication.Instance.Setup(); WebApplication.Instance.Start(); } // ... }
Modify styles and scripts in the application project as demonstrated below:
File: MySolution.Web\Login.aspx.
<html> <head id="Head1" runat="server"> <title>Logon</title> <style> .LogonTemplate .UPOAuth .menuButtons .dxm-item { background-color: white !important; color: #2C86D3 !important; border: 1px solid #d3d3d3 !important; } .LogonTemplate .UPOAuth .menuButtons .dxm-item .dx-vam { color: #2C86D3 !important; } .LogonTemplate .UPOAuth .menuButtons .dxm-item.dxm-hovered { background-color: white !important; color: #2C86D3 !important; border: 1px solid #d3d3d3 !important; } .LogonTemplate .UPOAuth .menuButtons.menuButtons_XafTheme .dxm-item a.dx { padding: 7px 21px 7px 21px !important; width: 105px; } .LogonTemplate .UPOAuth .dxm-spacing { padding: 0 !important; } .LogonTemplate .UPOAuth .menuButtons.menuButtons_XafTheme .dxm-item a.dx .dx-vam { padding-left: 5px; } .LogonTemplate .UPOAuth .menuActionImageSVG .dxm-image, .LogonTemplate .UPOAuth .dxm-popup .menuActionImageSVG .dxm-image, .LogonTemplate .UPOAuth .smallImage2 .dxm-image, .LogonTemplate .UPOAuth .dxm-popup .smallImage2 .dxm-image { padding: 3px 4px 3px 4px !important; } .LogonTemplate .UPOAuth .menuButtons.menuButtons_XafTheme .dxm-item.dxm-hovered a.dx { color: #2C86D3 !important; background-color: #F0F0F0 !important; background-image: none; } .LogonTemplate .UPOAuth .menuButtons .dxm-item { padding-left: 0px !important; padding-right: 0px !important; float: left; margin: 8px 8px 0 0; } .LogonTemplate .UPOAuth .menuButtons .dxm-item.LoginWithMicrosoft, .LogonTemplate .UPOAuth .menuButtons.menuButtons_XafTheme .dxm-item.LoginWithMicrosoft.dxm-hovered a.dx, .LogonTemplate .UPOAuth .menuButtons .dxm-item.LoginWithMicrosoft .dx-vam { background-color: #2c86d3 !important; color: #fff !important; border: none !important; } .StaticText { color: #9a9a9a; font-weight: bold; font-size: 17px; } </style> <script> function SetVisibleUserExistingText(visible) { function SetVisible() { if (visible) { document.getElementById('UseExisting').style.display = 'table'; } else { document.getElementById('UseExisting').style.display = 'none'; } } if (document.getElementById('UseExisting')) { SetVisible(); } else { document.addEventListener("DOMContentLoaded", function () { SetVisible(); }); } } </script> </head> <!-- ... --> </html>
If you want to display an icon on the authentication button, add the corresponding SVG image to the application Module project (MySolution.Module.Web). Set its Build Action property to Embedded Resource.
For example, you can use the following icon:
Customize the Default User Class
Extend the default user class with the following properties and add the
EmailEntity
class.File: MySolution.Module\BusinessObjects\ApplicationUser.cs(.vb).
using System.Collections.Generic; using System.ComponentModel; using System.Linq; using DevExpress.ExpressApp; using DevExpress.ExpressApp.Security; using DevExpress.Persistent.BaseImpl; using DevExpress.Persistent.BaseImpl.PermissionPolicy; using DevExpress.Persistent.Validation; using DevExpress.Xpo; namespace MySolution.Module.BusinessObjects { // ... [MapInheritance(MapInheritanceType.ParentTable)] [DefaultProperty(nameof(UserName))] public class ApplicationUser : PermissionPolicyUser, IObjectSpaceLink, ISecurityUserWithLoginInfo { // ... public bool EnableStandardAuthentication { get { return GetPropertyValue<bool>(nameof(EnableStandardAuthentication)); } set { SetPropertyValue(nameof(EnableStandardAuthentication), value); } } [Association, Aggregated] public XPCollection<EmailEntity> OAuthAuthenticationEmails { get { return GetCollection<EmailEntity>(nameof(OAuthAuthenticationEmails)); } } } public class EmailEntity : BaseObject { public EmailEntity(Session session) : base(session) { } [RuleUniqueValue("Unique_Email", DefaultContexts.Save, CriteriaEvaluationBehavior = CriteriaEvaluationBehavior.BeforeTransaction)] public string Email { get { return GetPropertyValue<string>(nameof(Email)); } set { SetPropertyValue(nameof(Email), value); } } [Association] public ApplicationUser ApplicationUser { get { return GetPropertyValue<ApplicationUser>(nameof(ApplicationUser)); } set { SetPropertyValue(nameof(ApplicationUser), value); } } } }
When users log on with their Microsoft account, the Security System creates a new user object with the email address as the user name. Each user can have several associated email addresses. To add or remove an email address, use the OAuth Authorization Emails list in the user’s Detail View.
You can specify the auto-assigned role for new users in the application’s InitializeComponent method:
File: MySolution.Web\WebApplication.cs.
public partial class MySolutionAspNetApplication : WebApplication { // ... private void InitializeComponent() { // ... this.securityStrategyComplex1.NewUserRoleName = "Default"; } // ... }
In the
Updater
class, set theEnableStandardAuthentication
property totrue
for predefined users who can login with standard authentication (username and password):File: MySolution.Module\DatabaseUpdate\Updater.cs.
public class Updater : ModuleUpdater { // ... public override void UpdateDatabaseAfterUpdateSchema() { base.UpdateDatabaseAfterUpdateSchema(); ApplicationUser sampleUser = ObjectSpace.FirstOrDefault<ApplicationUser>(u => u.UserName == "User"); if(sampleUser == null) { // ... sampleUser.EnableStandardAuthentication = true; ObjectSpace.CommitChanges(); } // ... } }
Note that the password of an autocreated user is empty. This means that such users can log on with their email and an empty password. You need to change this password when you enable standard authentication.
Implement Custom Authentication
Implement the custom authentication provider in the platform-agnostic Module project:
File: MySolution.Module\Security\CustomAuthenticationStandardProvider.cs.
using DevExpress.ExpressApp; using DevExpress.ExpressApp.Security; using MySolution.Module.BusinessObjects; using System; namespace MySolution.Module.Security { // ... public class CustomAuthenticationStandardProvider : AuthenticationStandardProvider { public CustomAuthenticationStandardProvider(Type userType) : base(userType) { } public override object Authenticate(IObjectSpace objectSpace) { ApplicationUser user = base.Authenticate(objectSpace) as ApplicationUser; if (user != null && !user.EnableStandardAuthentication) { throw new InvalidOperationException("Password authentication is not allowed for this user."); } return user; } } }
Create a SecurityStrategyComplex class descendant in the platform-specific Module project:
File: MySolution.Module.Web\Security\CustomSecurityStrategyComplex.cs.
using DevExpress.ExpressApp; using DevExpress.ExpressApp.Security; using System; using System.Web; namespace MySolution.Module.Web.Security { public class CustomSecurityStrategyComplex : SecurityStrategyComplex { protected override void InitializeNewUserCore(IObjectSpace objectSpace, object user) { base.InitializeNewUserCore(objectSpace, user); } public void InitializeNewUser(IObjectSpace objectSpace, object user) { InitializeNewUserCore(objectSpace, user); } public override void Logoff() { if (HttpContext.Current.Request.Cookies[".AspNet.External"] != null) { HttpContext.Current.Response.Cookies[".AspNet.External"].Expires = DateTime.Now.AddDays(-1); } if (HttpContext.Current.Request.Cookies[".AspNet.Cookies"] != null) { HttpContext.Current.Response.Cookies[".AspNet.Cookies"].Expires = DateTime.Now.AddDays(-1); } base.Logoff(); } } }
Add the additional ASP.NET Web Forms-specific authentication provider in the application project (MySolution.Web):
File: MySolution.Web\Security\OAuthProvider.cs.
using System; using System.Linq; using System.Web; using DevExpress.Data.Filtering; using DevExpress.ExpressApp; using DevExpress.ExpressApp.Security; using DevExpress.ExpressApp.Utils; using DevExpress.ExpressApp.Web; using Microsoft.Owin.Security; using MySolution.Module.BusinessObjects; using MySolution.Module.Web.Security; namespace MySolution.Web.Security { public class OAuthProvider : IAuthenticationProvider { private readonly Type userType; private readonly SecurityStrategyComplex security; public bool CreateUserAutomatically { get; set; } public OAuthProvider(Type userType, SecurityStrategyComplex security) { Guard.ArgumentNotNull(userType, "userType"); this.userType = userType; this.security = security; } public object Authenticate(IObjectSpace objectSpace) { ApplicationUser user = null; string userEmail = GetUserEmail(); if(!string.IsNullOrEmpty(userEmail)) { user = (ApplicationUser)objectSpace.FindObject(userType, CriteriaOperator.Parse("OAuthAuthenticationEmails[Email = ?]", userEmail)); if(user == null && CreateUserAutomatically) { user = (ApplicationUser)objectSpace.CreateObject(userType); user.UserName = userEmail; EmailEntity email = objectSpace.CreateObject<EmailEntity>(); email.Email = userEmail; user.OAuthAuthenticationEmails.Add(email); ((CustomSecurityStrategyComplex)security).InitializeNewUser(objectSpace, user); objectSpace.CommitChanges(); } } else { WebApplication.Redirect(WebApplication.LogonPage); } if(user == null) { throw new Exception("Login failed"); } return user; } private string GetUserEmail() { var authentication = HttpContext.Current.GetOwinContext().Authentication; var externalLoginInfo = authentication.GetExternalLoginInfo(); if(externalLoginInfo != null) { return externalLoginInfo.Email; } var email = authentication.User.Claims.Where(c => c.Type == Microsoft.Identity.Web.ClaimConstants.PreferredUserName).Select(c => c.Value).FirstOrDefault(); return email; } public void Setup(params object[] args) { } } }
Replace the default
SecurityStrategyComplex
used in your application with the customCustomSecurityStrategyComplex
. Also, useAuthenticationMixed
instead of your authentication:File: MySolution.Web\WebApplication.cs.
using DevExpress.ExpressApp.Security; // ... public partial class MySolutionAspNetApplication : WebApplication { public MySolutionAspNetApplication() { InitializeComponent(); //... AuthenticationMixed authenticationMixed = new AuthenticationMixed(); authenticationMixed.LogonParametersType = typeof(AuthenticationStandardLogonParameters); authenticationMixed.AuthenticationProviders.Add(typeof(CustomAuthenticationStandardProvider).Name, new CustomAuthenticationStandardProvider(typeof(ApplicationUser))); OAuthProvider authProvider = new OAuthProvider(typeof(ApplicationUser), securityStrategyComplex1); // Set the property below to "false" to disable autocreation of user objects. authProvider.CreateUserAutomatically = true; authenticationMixed.AuthenticationProviders.Add(typeof(OAuthProvider).Name, authProvider); securityStrategyComplex1.Authentication = authenticationMixed; } // ... private void InitializeComponent() { // ... this.securityStrategyComplex1 = new CustomSecurityStrategyComplex(); // ... } }
When
CreateUserAutomatically
is set tofalse
, the Security System allows logon if a user with the email returned by the external service exists in the application database. To grant access to a user with a specific email, use an admin account, create a user object, and set theUserName
to this email.
Implement the Logon Controller
Create the Controller in the platform-specific Module project:
File: MySolution.Module.Web\Controllers\LogonAuthController.cs.
using System; using System.Collections.Generic; using System.Web; using System.Web.UI; using DevExpress.ExpressApp; using DevExpress.ExpressApp.Actions; using DevExpress.ExpressApp.Security; using DevExpress.ExpressApp.Web; using DevExpress.ExpressApp.Web.Utils; using Microsoft.Owin.Security; using Microsoft.Owin.Security.OpenIdConnect; namespace MySolution.Module.Web.Controllers { public class LogonAuthController : ViewController<DetailView> { public const string OAuthParameter = "oauth"; private SimpleAction microsoftAction; public LogonAuthController() { TargetObjectType = typeof(AuthenticationStandardLogonParameters); microsoftAction = new SimpleAction(this, "LoginWithMicrosoft", "OAuthActions"); microsoftAction.Execute += microsoftAccountAction_Execute; microsoftAction.Caption = "Microsoft"; } protected override void OnActivated() { base.OnActivated(); LogonController logonController = Frame.GetController<LogonController>(); if (logonController != null) { logonController.AcceptAction.Changed += AcceptAction_Changed; } } private void Challenge(string provider) { var uriBuilder = new UriBuilder(HttpContext.Current.Request.Url); var queryString = HttpUtility.ParseQueryString(uriBuilder.Query); queryString["oauth"] = "true"; string redirectUrl = WebApplication.LogonPage + "?" + queryString.ToString(); AuthenticationProperties properties = new AuthenticationProperties(); properties.RedirectUri = redirectUrl; properties.Dictionary["Provider"] = provider; HttpContext.Current.GetOwinContext().Authentication.Challenge(properties, provider); } private void microsoftAccountAction_Execute(object sender, SimpleActionExecuteEventArgs e) { Challenge(OpenIdConnectAuthenticationDefaults.AuthenticationType); } private IList<string> GetProviderNames() { IList<AuthenticationDescription> descriptions = HttpContext.Current.GetOwinContext().Authentication.GetAuthenticationTypes((AuthenticationDescription d) => d.Properties != null && d.Properties.ContainsKey("Caption")) as IList<AuthenticationDescription>; List<string> providersNames = new List<string>(); foreach (AuthenticationDescription description in descriptions) { providersNames.Add(description.AuthenticationType); } return providersNames; } private void CurrentRequestPage_Load(object sender, System.EventArgs e) { ((Page)sender).Load -= CurrentRequestPage_Load; LogonController logonController = Frame.GetController<LogonController>(); if (logonController != null && logonController.AcceptAction.Active) { ((ISupportMixedAuthentication)Application.Security).AuthenticationMixed.SetupAuthenticationProvider("OAuthProvider"); logonController.AcceptAction.DoExecute(); } } private void AcceptAction_Changed(object sender, ActionChangedEventArgs e) { if (e.ChangedPropertyType == ActionChangedType.Active) { SetActionsActive(((ActionBase)sender).Active); } } private void SetActionsActive(bool logonActionActive) { foreach (ActionBase action in Actions) { action.Active["LogonActionActive"] = logonActionActive; } RegisterVisibleUserExistingTextScript(logonActionActive); } private void RegisterVisibleUserExistingTextScript(bool visible) { ((WebWindow)Frame).RegisterClientScript("LogonActionActive", string.Format("SetVisibleUserExistingText({0});", ClientSideEventsHelper.ToJSBoolean(visible)), true); } protected override void OnViewControlsCreated() { LogonController logonController = Frame.GetController<LogonController>(); if (logonController != null) { SetActionsActive(logonController.AcceptAction.Active); } IList<string> providersName = GetProviderNames() as IList<string>; if (providersName.Count == 0) { RegisterVisibleUserExistingTextScript(false); } microsoftAction.Active["ProviderIsSet"] = providersName.Contains(OpenIdConnectAuthenticationDefaults.AuthenticationType); if (IsOAuthRequest && WebWindow.CurrentRequestPage != null) { WebWindow.CurrentRequestPage.Load += CurrentRequestPage_Load; } base.OnViewControlsCreated(); } public static bool IsOAuthRequest { get { return HttpContext.Current.Request.Params[OAuthParameter] != null; } } protected override void OnDeactivated() { LogonController logonController = Frame.GetController<LogonController>(); if (logonController != null) { logonController.AcceptAction.Changed -= AcceptAction_Changed; } base.OnDeactivated(); } } }
In the overridden
Setup
method of the platform-specific Module, handle the XafApplication.CreateCustomLogonWindowControllers event and add theLogonAuthController
to the e.Controllers collection passed to this event.File: MySolution.Module.Web\WebModule.cs.
public sealed partial class MySolutionAspNetModule : ModuleBase { // ... public override void Setup(XafApplication application) { base.Setup(application); application.CreateCustomLogonWindowControllers += Application_CreateCustomLogonWindowControllers; } private void Application_CreateCustomLogonWindowControllers(object sender, CreateCustomLogonWindowControllersEventArgs e) { XafApplication app = (XafApplication)sender; e.Controllers.Add(app.CreateController<LogonAuthController>()); } }
Customize the Application Model
Rebuild your solution and invoke the Model Editor for the application project.
Navigate to the ActionDesign | Actions | LoginWithMicrosoft node and specify the following properties:
Property Value ImageName Microsoft PaintStyle CaptionAndImage CustomCSSClassName LoginWithMicrosoft IsPostBackRequired true Navigate to the Views | DevExpress.ExpressApp.Security | AuthenticationStandardLogonParameters_DetailView | Items | LogonText node and set the text property to Login to your account.