summaryrefslogtreecommitdiffstats
path: root/src/OpenID/OpenIdProviderMvc/Controllers
diff options
context:
space:
mode:
Diffstat (limited to 'src/OpenID/OpenIdProviderMvc/Controllers')
-rw-r--r--src/OpenID/OpenIdProviderMvc/Controllers/AccountController.cs226
-rw-r--r--src/OpenID/OpenIdProviderMvc/Controllers/HomeController.cs29
-rw-r--r--src/OpenID/OpenIdProviderMvc/Controllers/OpenIdController.cs221
-rw-r--r--src/OpenID/OpenIdProviderMvc/Controllers/UserController.cs48
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;
+ }
+ }
+}