namespace MvcRelyingParty.Controllers { using System; using System.Collections.Generic; 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 DotNetOpenAuth.Messaging; using DotNetOpenAuth.OAuth; using DotNetOpenAuth.OAuth.Messages; using DotNetOpenAuth.OpenId; using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration; using DotNetOpenAuth.OpenId.RelyingParty; using MvcRelyingParty.Models; using RelyingPartyLogic; [HandleError] public class AccountController : Controller { /// /// Initializes a new instance of the class. /// /// /// This constructor is used by the MVC framework to instantiate the controller using /// the default forms authentication and OpenID services. /// public AccountController() : this(null, null) { } /// /// Initializes a new instance of the class. /// /// The forms auth. /// The relying party. /// /// This constructor is not used by the MVC framework but is instead provided for ease /// of unit testing this type. /// public AccountController(IFormsAuthentication formsAuth, IOpenIdRelyingParty relyingParty) { this.FormsAuth = formsAuth ?? new FormsAuthenticationService(); this.RelyingParty = relyingParty ?? new OpenIdRelyingPartyService(); } /// /// Gets the forms authentication module to use. /// public IFormsAuthentication FormsAuth { get; private set; } /// /// Gets the OpenID relying party to use for logging users in. /// public IOpenIdRelyingParty RelyingParty { get; private set; } /// /// Prepares a web page to help the user supply his login information. /// /// The action result. public ActionResult LogOn() { return View(); } /// /// Accepts the login information provided by the user and redirects /// the user to their Provider to complete authentication. /// /// The user-supplied identifier. /// Whether the user wants a persistent cookie. /// The URL to direct the user to after successfully authenticating. /// The action result. [AcceptVerbs(HttpVerbs.Post), ValidateAntiForgeryToken] public ActionResult LogOn(string openid_identifier, bool rememberMe, string returnUrl) { Identifier userSuppliedIdentifier; if (Identifier.TryParse(openid_identifier, out userSuppliedIdentifier)) { try { var request = this.RelyingParty.CreateRequest(openid_identifier, Realm.AutoDetect, Url.ActionFull("LogOnReturnTo")); request.SetUntrustedCallbackArgument("rememberMe", rememberMe ? "1" : "0"); // This might be signed so the OP can't send the user to a dangerous URL. // Of course, if that itself was a danger then the site is vulnerable to XSRF attacks anyway. if (!string.IsNullOrEmpty(returnUrl)) { request.SetUntrustedCallbackArgument("returnUrl", returnUrl); } // Ask for the user's email, not because we necessarily need it to do our work, // but so we can display something meaningful to the user as their "username" // when they log in with a PPID from Google, for example. request.AddExtension(new ClaimsRequest { Email = DemandLevel.Require, FullName = DemandLevel.Request, PolicyUrl = Url.ActionFull("PrivacyPolicy", "Home"), }); return request.RedirectingResponse.AsActionResult(); } catch (ProtocolException ex) { ModelState.AddModelError("OpenID", ex.Message); } } else { ModelState.AddModelError("openid_identifier", "This doesn't look like a valid OpenID."); } return View(); } /// /// Handles the positive assertion that comes from Providers to Javascript running in the browser. /// /// The action result. /// /// This method instructs ASP.NET MVC to not validate input /// because some OpenID positive assertions messages otherwise look like /// hack attempts and result in errors when validation is turned on. /// [AcceptVerbs(HttpVerbs.Get | HttpVerbs.Post), ValidateInput(false)] public ActionResult LogOnReturnToAjax() { return RelyingPartyUtilities.AjaxReturnTo(this.Request); } /// /// Handles the positive assertion that comes from Providers. /// /// The action result. /// /// This method instructs ASP.NET MVC to not validate input /// because some OpenID positive assertions messages otherwise look like /// hack attempts and result in errors when validation is turned on. /// [AcceptVerbs(HttpVerbs.Get | HttpVerbs.Post), ValidateInput(false)] public ActionResult LogOnReturnTo() { var response = this.RelyingParty.GetResponse(); if (response != null) { switch (response.Status) { case AuthenticationStatus.Authenticated: var token = RelyingPartyLogic.User.ProcessUserLogin(response); bool rememberMe = response.GetUntrustedCallbackArgument("rememberMe") == "1"; this.FormsAuth.SignIn(token.ClaimedIdentifier, rememberMe); string returnUrl = response.GetUntrustedCallbackArgument("returnUrl"); if (!String.IsNullOrEmpty(returnUrl)) { return Redirect(returnUrl); } else { return RedirectToAction("Index", "Home"); } case AuthenticationStatus.Canceled: ModelState.AddModelError("OpenID", "It looks like you canceled login at your OpenID Provider."); break; case AuthenticationStatus.Failed: ModelState.AddModelError("OpenID", response.Exception.Message); break; } } // If we're to this point, login didn't complete successfully. // Show the LogOn view again to show the user any errors and // give another chance to complete login. return View("LogOn"); } /// /// Logs the user out of the site and redirects the browser to our home page. /// /// The action result. public ActionResult LogOff() { this.FormsAuth.SignOut(); return RedirectToAction("Index", "Home"); } public JsonResult Discover(string identifier) { if (!this.Request.IsAjaxRequest()) { throw new InvalidOperationException(); } return RelyingPartyUtilities.AjaxDiscover(identifier, Realm.AutoDetect, Url.ActionFull("LogOnReturnToAjax")); } [Authorize] public ActionResult Edit() { return View(GetAccountInfoModel()); } /// /// Updates the user's account information. /// /// The first name. /// The last name. /// The email address. /// An updated view showing the new profile. /// /// This action accepts PUT because this operation is idempotent in nature. /// [Authorize, AcceptVerbs(HttpVerbs.Put), ValidateAntiForgeryToken] public ActionResult Update(string firstName, string lastName, string emailAddress) { Database.LoggedInUser.FirstName = firstName; Database.LoggedInUser.LastName = lastName; if (Database.LoggedInUser.EmailAddress != emailAddress) { Database.LoggedInUser.EmailAddress = emailAddress; Database.LoggedInUser.EmailAddressVerified = false; } return PartialView("EditFields", GetAccountInfoModel()); } [Authorize] public ActionResult Authorize() { if (OAuthServiceProvider.PendingAuthorizationRequest == null) { return RedirectToAction("Edit"); } var model = new AccountAuthorizeModel { ConsumerApp = OAuthServiceProvider.PendingAuthorizationConsumer.Name, IsUnsafeRequest = OAuthServiceProvider.PendingAuthorizationRequest.IsUnsafeRequest, }; return View(model); } [Authorize, AcceptVerbs(HttpVerbs.Post), ValidateAntiForgeryToken] public ActionResult Authorize(bool isApproved) { if (isApproved) { var consumer = OAuthServiceProvider.PendingAuthorizationConsumer; var tokenManager = OAuthServiceProvider.ServiceProvider.TokenManager; var pendingRequest = OAuthServiceProvider.PendingAuthorizationRequest; ITokenContainingMessage requestTokenMessage = pendingRequest; var requestToken = tokenManager.GetRequestToken(requestTokenMessage.Token); var response = OAuthServiceProvider.AuthorizePendingRequestTokenAsWebResponse(); if (response != null) { // The consumer provided a callback URL that can take care of everything else. return response.AsActionResult(); } var model = new AccountAuthorizeModel { ConsumerApp = consumer.Name, }; if (!pendingRequest.IsUnsafeRequest) { model.VerificationCode = ServiceProvider.CreateVerificationCode(consumer.VerificationCodeFormat, consumer.VerificationCodeLength); requestToken.VerificationCode = model.VerificationCode; tokenManager.UpdateToken(requestToken); } return View("AuthorizeApproved", model); } else { OAuthServiceProvider.PendingAuthorizationRequest = null; return View("AuthorizeDenied"); } } [Authorize, AcceptVerbs(HttpVerbs.Delete)] // ValidateAntiForgeryToken would be GREAT here, but it's not a FORM POST operation so that doesn't work. public ActionResult RevokeToken(string token) { if (String.IsNullOrEmpty(token)) { throw new ArgumentNullException("token"); } var tokenEntity = Database.DataContext.IssuedTokens.OfType().Where(t => t.User.UserId == Database.LoggedInUser.UserId && t.Token == token).FirstOrDefault(); if (tokenEntity == null) { throw new ArgumentOutOfRangeException("id", "The logged in user does not have a token with this name to revoke."); } Database.DataContext.DeleteObject(tokenEntity); Database.DataContext.SaveChanges(); // make changes now so the model we fill up reflects the change return PartialView("AuthorizedApps", GetAccountInfoModel()); } [Authorize, AcceptVerbs(HttpVerbs.Get | HttpVerbs.Post)] public ActionResult AddAuthenticationTokenReturnTo(string openid_identifier) { var response = this.RelyingParty.GetResponse(); if (response != null) { switch (response.Status) { case AuthenticationStatus.Authenticated: Database.LoggedInUser.AuthenticationTokens.Add(new AuthenticationToken { ClaimedIdentifier = response.ClaimedIdentifier, FriendlyIdentifier = response.FriendlyIdentifierForDisplay, }); Database.DataContext.SaveChanges(); break; default: break; } } return RedirectToAction("Edit"); } [Authorize, AcceptVerbs(HttpVerbs.Post), ValidateAntiForgeryToken] public ActionResult AddAuthenticationToken(string openid_identifier) { Identifier userSuppliedIdentifier; if (Identifier.TryParse(openid_identifier, out userSuppliedIdentifier)) { try { var request = this.RelyingParty.CreateRequest(userSuppliedIdentifier, Realm.AutoDetect, Url.ActionFull("AddAuthenticationTokenReturnTo")); return request.RedirectingResponse.AsActionResult(); } catch (ProtocolException ex) { ModelState.AddModelError("openid_identifier", ex); } } else { ModelState.AddModelError("openid_identifier", "This doesn't look like a valid OpenID."); } return View("Edit", GetAccountInfoModel()); } private static AccountInfoModel GetAccountInfoModel() { var authorizedApps = from token in Database.DataContext.IssuedTokens.OfType() where token.User.UserId == Database.LoggedInUser.UserId select new AccountInfoModel.AuthorizedApp { AppName = token.Consumer.Name, Token = token.Token }; Database.LoggedInUser.AuthenticationTokens.Load(); var model = new AccountInfoModel { FirstName = Database.LoggedInUser.FirstName, LastName = Database.LoggedInUser.LastName, EmailAddress = Database.LoggedInUser.EmailAddress, AuthorizedApps = authorizedApps.ToList(), AuthenticationTokens = Database.LoggedInUser.AuthenticationTokens.ToList(), }; return model; } } }