diff options
Diffstat (limited to 'src/OpenID/OpenIdProviderMvc/Controllers')
4 files changed, 524 insertions, 0 deletions
diff --git a/src/OpenID/OpenIdProviderMvc/Controllers/AccountController.cs b/src/OpenID/OpenIdProviderMvc/Controllers/AccountController.cs new file mode 100644 index 0000000..7cb4b62 --- /dev/null +++ b/src/OpenID/OpenIdProviderMvc/Controllers/AccountController.cs @@ -0,0 +1,226 @@ +namespace OpenIdProviderMvc.Controllers { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.Linq; + using System.Security.Principal; + using System.Web; + using System.Web.Mvc; + using System.Web.Security; + using System.Web.UI; + using OpenIdProviderMvc.Code; + + [HandleError] + public class AccountController : Controller { + /// <summary> + /// Initializes a new instance of the <see cref="AccountController"/> class. + /// </summary> + /// <remarks> + /// This constructor is used by the MVC framework to instantiate the controller using + /// the default forms authentication and membership providers. + /// </remarks> + public AccountController() + : this(null, null) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="AccountController"/> class. + /// </summary> + /// <param name="formsAuth">The forms authentication service.</param> + /// <param name="service">The membership service.</param> + /// <remarks> + /// This constructor is not used by the MVC framework but is instead provided for ease + /// of unit testing this type. See the comments at the end of this file for more + /// information. + /// </remarks> + public AccountController(IFormsAuthentication formsAuth, IMembershipService service) { + this.FormsAuth = formsAuth ?? new FormsAuthenticationService(); + this.MembershipService = service ?? new AccountMembershipService(); + } + + public IFormsAuthentication FormsAuth { get; private set; } + + public IMembershipService MembershipService { get; private set; } + + public ActionResult LogOn() { + return View(); + } + + [AcceptVerbs(HttpVerbs.Post)] + [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", Justification = "Needs to take same parameter type as Controller.Redirect()")] + public ActionResult LogOn(string userName, string password, bool rememberMe, string returnUrl) { + if (!this.ValidateLogOn(userName, password)) { + return View(); + } + + this.FormsAuth.SignIn(userName, rememberMe); + if (!string.IsNullOrEmpty(returnUrl)) { + return Redirect(returnUrl); + } else { + return RedirectToAction("Index", "Home"); + } + } + + public ActionResult LogOff() { + this.FormsAuth.SignOut(); + + return RedirectToAction("Index", "Home"); + } + + public ActionResult Register() { + ViewData["PasswordLength"] = this.MembershipService.MinPasswordLength; + + return View(); + } + + [AcceptVerbs(HttpVerbs.Post)] + public ActionResult Register(string userName, string email, string password, string confirmPassword) { + this.ViewData["PasswordLength"] = this.MembershipService.MinPasswordLength; + + if (this.ValidateRegistration(userName, email, password, confirmPassword)) { + // Attempt to register the user + MembershipCreateStatus createStatus = this.MembershipService.CreateUser(userName, password, email); + + if (createStatus == MembershipCreateStatus.Success) { + this.FormsAuth.SignIn(userName, false /* createPersistentCookie */); + return RedirectToAction("Index", "Home"); + } else { + ModelState.AddModelError("_FORM", ErrorCodeToString(createStatus)); + } + } + + // If we got this far, something failed, redisplay form + return View(); + } + + [Authorize] + public ActionResult ChangePassword() { + ViewData["PasswordLength"] = this.MembershipService.MinPasswordLength; + + return View(); + } + + [Authorize] + [AcceptVerbs(HttpVerbs.Post)] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions result in password not being changed.")] + public ActionResult ChangePassword(string currentPassword, string newPassword, string confirmPassword) { + ViewData["PasswordLength"] = this.MembershipService.MinPasswordLength; + + if (!this.ValidateChangePassword(currentPassword, newPassword, confirmPassword)) { + return View(); + } + + try { + if (this.MembershipService.ChangePassword(User.Identity.Name, currentPassword, newPassword)) { + return RedirectToAction("ChangePasswordSuccess"); + } else { + ModelState.AddModelError("_FORM", "The current password is incorrect or the new password is invalid."); + return View(); + } + } catch { + ModelState.AddModelError("_FORM", "The current password is incorrect or the new password is invalid."); + return View(); + } + } + + public ActionResult ChangePasswordSuccess() { + return View(); + } + + protected override void OnActionExecuting(ActionExecutingContext filterContext) { + if (filterContext.HttpContext.User.Identity is WindowsIdentity) { + throw new InvalidOperationException("Windows authentication is not supported."); + } + } + + #region Validation Methods + + private static string ErrorCodeToString(MembershipCreateStatus createStatus) { + // See http://msdn.microsoft.com/en-us/library/system.web.security.membershipcreatestatus.aspx for + // a full list of status codes. + switch (createStatus) { + case MembershipCreateStatus.DuplicateUserName: + return "Username already exists. Please enter a different user name."; + + case MembershipCreateStatus.DuplicateEmail: + return "A username for that e-mail address already exists. Please enter a different e-mail address."; + + case MembershipCreateStatus.InvalidPassword: + return "The password provided is invalid. Please enter a valid password value."; + + case MembershipCreateStatus.InvalidEmail: + return "The e-mail address provided is invalid. Please check the value and try again."; + + case MembershipCreateStatus.InvalidAnswer: + return "The password retrieval answer provided is invalid. Please check the value and try again."; + + case MembershipCreateStatus.InvalidQuestion: + return "The password retrieval question provided is invalid. Please check the value and try again."; + + case MembershipCreateStatus.InvalidUserName: + return "The user name provided is invalid. Please check the value and try again."; + + case MembershipCreateStatus.ProviderError: + return "The authentication provider returned an error. Please verify your entry and try again. If the problem persists, please contact your system administrator."; + + case MembershipCreateStatus.UserRejected: + return "The user creation request has been canceled. Please verify your entry and try again. If the problem persists, please contact your system administrator."; + + default: + return "An unknown error occurred. Please verify your entry and try again. If the problem persists, please contact your system administrator."; + } + } + + private bool ValidateChangePassword(string currentPassword, string newPassword, string confirmPassword) { + if (string.IsNullOrEmpty(currentPassword)) { + ModelState.AddModelError("currentPassword", "You must specify a current password."); + } + if (newPassword == null || newPassword.Length < this.MembershipService.MinPasswordLength) { + ModelState.AddModelError( + "newPassword", + string.Format(CultureInfo.CurrentCulture, "You must specify a new password of {0} or more characters.", this.MembershipService.MinPasswordLength)); + } + + if (!string.Equals(newPassword, confirmPassword, StringComparison.Ordinal)) { + ModelState.AddModelError("_FORM", "The new password and confirmation password do not match."); + } + + return ModelState.IsValid; + } + + private bool ValidateLogOn(string userName, string password) { + if (string.IsNullOrEmpty(userName)) { + ModelState.AddModelError("username", "You must specify a username."); + } + if (string.IsNullOrEmpty(password)) { + ModelState.AddModelError("password", "You must specify a password."); + } + if (!this.MembershipService.ValidateUser(userName, password)) { + ModelState.AddModelError("_FORM", "The username or password provided is incorrect."); + } + + return ModelState.IsValid; + } + + private bool ValidateRegistration(string userName, string email, string password, string confirmPassword) { + if (string.IsNullOrEmpty(userName)) { + ModelState.AddModelError("username", "You must specify a username."); + } + if (string.IsNullOrEmpty(email)) { + ModelState.AddModelError("email", "You must specify an email address."); + } + if (password == null || password.Length < this.MembershipService.MinPasswordLength) { + ModelState.AddModelError( + "password", + string.Format(CultureInfo.CurrentCulture, "You must specify a password of {0} or more characters.", this.MembershipService.MinPasswordLength)); + } + if (!string.Equals(password, confirmPassword, StringComparison.Ordinal)) { + ModelState.AddModelError("_FORM", "The new password and confirmation password do not match."); + } + return ModelState.IsValid; + } + + #endregion + } +} diff --git a/src/OpenID/OpenIdProviderMvc/Controllers/HomeController.cs b/src/OpenID/OpenIdProviderMvc/Controllers/HomeController.cs new file mode 100644 index 0000000..fb03ce2 --- /dev/null +++ b/src/OpenID/OpenIdProviderMvc/Controllers/HomeController.cs @@ -0,0 +1,29 @@ +namespace OpenIdProviderMvc.Controllers { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Web; + using System.Web.Mvc; + + [HandleError] + public class HomeController : Controller { + public ActionResult Index() { + if (Request.AcceptTypes.Contains("application/xrds+xml")) { + ViewData["OPIdentifier"] = true; + return View("Xrds"); + } + + ViewData["Message"] = "Welcome to ASP.NET MVC!"; + return View(); + } + + public ActionResult About() { + return View(); + } + + public ActionResult Xrds() { + ViewData["OPIdentifier"] = true; + return View(); + } + } +} diff --git a/src/OpenID/OpenIdProviderMvc/Controllers/OpenIdController.cs b/src/OpenID/OpenIdProviderMvc/Controllers/OpenIdController.cs new file mode 100644 index 0000000..198c434 --- /dev/null +++ b/src/OpenID/OpenIdProviderMvc/Controllers/OpenIdController.cs @@ -0,0 +1,221 @@ +namespace OpenIdProviderMvc.Controllers { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Web; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId; + using DotNetOpenAuth.OpenId.Behaviors; + using DotNetOpenAuth.OpenId.Extensions.ProviderAuthenticationPolicy; + using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration; + using DotNetOpenAuth.OpenId.Provider; + using DotNetOpenAuth.OpenId.Provider.Behaviors; + using OpenIdProviderMvc.Code; + + public class OpenIdController : Controller { + internal static OpenIdProvider OpenIdProvider = new OpenIdProvider(); + + [ValidateInput(false)] + public ActionResult Provider() { + IRequest request = OpenIdProvider.GetRequest(); + if (request != null) { + // Some requests are automatically handled by DotNetOpenAuth. If this is one, go ahead and let it go. + if (request.IsResponseReady) { + return OpenIdProvider.PrepareResponse(request).AsActionResult(); + } + + // This is apparently one that the host (the web site itself) has to respond to. + ProviderEndpoint.PendingRequest = (IHostProcessedRequest)request; + + // Try responding immediately if possible. + ActionResult response; + if (this.AutoRespondIfPossible(out response)) { + return response; + } + + // We can't respond immediately with a positive result. But if we still have to respond immediately... + if (ProviderEndpoint.PendingRequest.Immediate) { + // We can't stop to prompt the user -- we must just return a negative response. + return this.SendAssertion(); + } + + return this.RedirectToAction("AskUser"); + } else { + // No OpenID request was recognized. This may be a user that stumbled on the OP Endpoint. + return this.View(); + } + } + + /// <summary> + /// Displays a confirmation page. + /// </summary> + /// <returns>The response for the user agent.</returns> + [Authorize] + public ActionResult AskUser() { + if (ProviderEndpoint.PendingRequest == null) { + // Oops... precious little we can confirm without a pending OpenID request. + return this.RedirectToAction("Index", "Home"); + } + + // The user MAY have just logged in. Try again to respond automatically to the RP if appropriate. + ActionResult response; + if (this.AutoRespondIfPossible(out response)) { + return response; + } + + this.ViewData["Realm"] = ProviderEndpoint.PendingRequest.Realm; + + return this.View(); + } + + [HttpPost, Authorize, ValidateAntiForgeryToken] + public ActionResult AskUserResponse(bool confirmed) { + if (ProviderEndpoint.PendingAnonymousRequest != null) { + ProviderEndpoint.PendingAnonymousRequest.IsApproved = confirmed; + } else if (ProviderEndpoint.PendingAuthenticationRequest != null) { + ProviderEndpoint.PendingAuthenticationRequest.IsAuthenticated = confirmed; + } else { + throw new InvalidOperationException("There's no pending authentication request!"); + } + + return this.SendAssertion(); + } + + /// <summary> + /// Sends a positive or a negative assertion, based on how the pending request is currently marked. + /// </summary> + /// <returns>An MVC redirect result.</returns> + public ActionResult SendAssertion() { + var pendingRequest = ProviderEndpoint.PendingRequest; + var authReq = pendingRequest as IAuthenticationRequest; + var anonReq = pendingRequest as IAnonymousRequest; + ProviderEndpoint.PendingRequest = null; // clear session static so we don't do this again + if (pendingRequest == null) { + throw new InvalidOperationException("There's no pending authentication request!"); + } + + // Set safe defaults if somehow the user ended up (perhaps through XSRF) here before electing to send data to the RP. + if (anonReq != null && !anonReq.IsApproved.HasValue) { + anonReq.IsApproved = false; + } + + if (authReq != null && !authReq.IsAuthenticated.HasValue) { + authReq.IsAuthenticated = false; + } + + if (authReq != null && authReq.IsAuthenticated.Value) { + if (authReq.IsDirectedIdentity) { + authReq.LocalIdentifier = Models.User.GetClaimedIdentifierForUser(User.Identity.Name); + } + + if (!authReq.IsDelegatedIdentifier) { + authReq.ClaimedIdentifier = authReq.LocalIdentifier; + } + } + + // Respond to AX/sreg extension requests only on a positive result. + if ((authReq != null && authReq.IsAuthenticated.Value) || + (anonReq != null && anonReq.IsApproved.Value)) { + // Look for a Simple Registration request. When the AXFetchAsSregTransform behavior is turned on + // in the web.config file as it is in this sample, AX requests will come in as SReg requests. + var claimsRequest = pendingRequest.GetExtension<ClaimsRequest>(); + if (claimsRequest != null) { + var claimsResponse = claimsRequest.CreateResponse(); + + // This simple respond to a request check may be enhanced to only respond to an individual attribute + // request if the user consents to it explicitly, in which case this response extension creation can take + // place in the confirmation page action rather than here. + if (claimsRequest.Email != DemandLevel.NoRequest) { + claimsResponse.Email = User.Identity.Name + "@dotnetopenauth.net"; + } + + pendingRequest.AddResponseExtension(claimsResponse); + } + } + + return OpenIdProvider.PrepareResponse(pendingRequest).AsActionResult(); + } + + /// <summary> + /// Attempts to formulate an automatic response to the RP if the user's profile allows it. + /// </summary> + /// <param name="response">Receives the ActionResult for the caller to return, or <c>null</c> if no automatic response can be made.</param> + /// <returns>A value indicating whether an automatic response is possible.</returns> + private bool AutoRespondIfPossible(out ActionResult response) { + // If the odds are good we can respond to this one immediately (without prompting the user)... + if (ProviderEndpoint.PendingRequest.IsReturnUrlDiscoverable(OpenIdProvider.Channel.WebRequestHandler) == RelyingPartyDiscoveryResult.Success + && User.Identity.IsAuthenticated + && this.HasUserAuthorizedAutoLogin(ProviderEndpoint.PendingRequest)) { + // Is this is an identity authentication request? (as opposed to an anonymous request)... + if (ProviderEndpoint.PendingAuthenticationRequest != null) { + // If this is directed identity, or if the claimed identifier being checked is controlled by the current user... + if (ProviderEndpoint.PendingAuthenticationRequest.IsDirectedIdentity + || this.UserControlsIdentifier(ProviderEndpoint.PendingAuthenticationRequest)) { + ProviderEndpoint.PendingAuthenticationRequest.IsAuthenticated = true; + response = this.SendAssertion(); + return true; + } + } + + // If this is an anonymous request, we can respond to that too. + if (ProviderEndpoint.PendingAnonymousRequest != null) { + ProviderEndpoint.PendingAnonymousRequest.IsApproved = true; + response = this.SendAssertion(); + return true; + } + } + + response = null; + return false; + } + + /// <summary> + /// Determines whether the currently logged in user has authorized auto login to the requesting relying party. + /// </summary> + /// <param name="request">The incoming request.</param> + /// <returns> + /// <c>true</c> if it is safe to respond affirmatively to this request and all extensions + /// without further user confirmation; otherwise, <c>false</c>. + /// </returns> + private bool HasUserAuthorizedAutoLogin(IHostProcessedRequest request) { + // TODO: host should implement this method meaningfully, consulting their user database. + // Make sure the user likes the RP + if (true/*User.UserLikesRP(request.Realm))*/) { + // And make sure the RP is only asking for information about the user that the user has granted before. + if (true/*User.HasGrantedExtensions(request)*/) { + // For now for the purposes of the sample, we'll disallow auto-logins when an sreg request is present. + if (request.GetExtension<ClaimsRequest>() != null) { + return false; + } + + return true; + } + } + + // If we aren't sure the user likes this site and is willing to disclose the requested info, return false + // so the user has the opportunity to explicity choose whether to share his/her info. + return false; + } + + /// <summary> + /// Checks whether the logged in user controls the OP local identifier in the given authentication request. + /// </summary> + /// <param name="authReq">The authentication request.</param> + /// <returns><c>true</c> if the user controls the identifier; <c>false</c> otherwise.</returns> + private bool UserControlsIdentifier(IAuthenticationRequest authReq) { + if (authReq == null) { + throw new ArgumentNullException("authReq"); + } + + if (User == null || User.Identity == null) { + return false; + } + + Uri userLocalIdentifier = Models.User.GetClaimedIdentifierForUser(User.Identity.Name); + return authReq.LocalIdentifier == userLocalIdentifier || + authReq.LocalIdentifier == PpidGeneration.PpidIdentifierProvider.GetIdentifier(userLocalIdentifier, authReq.Realm); + } + } +} diff --git a/src/OpenID/OpenIdProviderMvc/Controllers/UserController.cs b/src/OpenID/OpenIdProviderMvc/Controllers/UserController.cs new file mode 100644 index 0000000..5e0c21f --- /dev/null +++ b/src/OpenID/OpenIdProviderMvc/Controllers/UserController.cs @@ -0,0 +1,48 @@ +namespace OpenIdProviderMvc.Controllers { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Web; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + + public class UserController : Controller { + /// <summary> + /// Identities the specified id. + /// </summary> + /// <param name="id">The username or anonymous identifier.</param> + /// <param name="anon">if set to <c>true</c> then <paramref name="id"/> represents an anonymous identifier rather than a username.</param> + /// <returns>The view to display.</returns> + public ActionResult Identity(string id, bool anon) { + if (!anon) { + var redirect = this.RedirectIfNotNormalizedRequestUri(id); + if (redirect != null) { + return redirect; + } + } + + if (Request.AcceptTypes != null && Request.AcceptTypes.Contains("application/xrds+xml")) { + return View("Xrds"); + } + + if (!anon) { + this.ViewData["username"] = id; + } + + return View(); + } + + public ActionResult Xrds(string id) { + return View(); + } + + private ActionResult RedirectIfNotNormalizedRequestUri(string user) { + Uri normalized = Models.User.GetClaimedIdentifierForUser(user); + if (Request.Url != normalized) { + return Redirect(normalized.AbsoluteUri); + } + + return null; + } + } +} |