OWIN Authentication Using Ultimate SAML

Overview

OWIN Authentication and SAML both are for security purpose in web applications. They provide an easy way to handle credentials security issues between web servers and web applications.

OWIN Authentication

OWIN stands for Open Web Interface. It defines an abstract layer of security to web applications and web servers. The primary purpose of OWIN is to provide a basic, pluggable design for .NET web applications and web servers whereupon they depend, empowering advancement of small, engaging application parts (known as "MIDDLEWARES" in the OWIN speech) which can be collected into a processing pipeline through which the server would then be able to route incoming HTTP requests.

SAML

On the other hand, SAML stands for Security Assertion Markup Language. It is the open standard to use one set of credentials to log in to multiple and different websites. It can be used for Single Sign-On (SSO), Identity Management (IDP), federation, Service Provider (SP) and SAML Assertion. For more detail study of SAML you can find here: What is SAML and how does it work?

SAML and OWIN terminology

Some of the SAML and OWIN Authentication terms are for comparative concepts. The following is a list that should clarify the similarities between them. SAML terms with OWIN Authentication equivalents in small brackets:

  • Identity Provider (Authorization Server) – The server that has user credentials and identities.
  • Service Provider (Resource Server) – The server that provides the services to the users.
  • Client – How the user is interacting with the resource server. e.g., web application through the browser.

Why use OWIN and SAML together?

OWIN is an interface to provide an interaction between an application and a server. You can say, OWIN is simply a specification, not an implementation. For the implementation, we can use .NET libraries provided by Microsoft or third-party libraries and tools. So SAML is the toolkit to implement an OWIN Authentication.

Implementation of OWIN Authentication using Ultimate SAML

First download the SAML library from here: SAML SSO Library. Once completed, install it on your machine and navigate to the CS or VB example to play with it. The full example is located in <Installed_Folder>\Samples\Saml\Mvc\CS\Saml2-AdvancedApi\Saml2OwinMvc. For VB example, replace the CS dir in that path with VB.

Below is the example illustrating how to implement OWIN authentication using Ultimate SAML.

Single Sign-On Service

Create SingleSignOnService Controller to implement single sign-on. Write following code in SingleSignOnServiceController class.

// The session key for saving the SSO state during a local login.
internal const string SsoSessionKey = "sso";        

public ActionResult Index()
{
    try
    {
        // Load the Single Sign-On state from the Session state.
        // If the saved authentication state is a null reference, receive the authentication request from the query string and form data.
        SsoAuthnState ssoState = (SsoAuthnState)Session[SsoSessionKey];

        if (ssoState == null || !HttpContext.User.Identity.IsAuthenticated)
        {
            // Receive the authentication request.
            AuthnRequest authnRequest;
            string relayState;

            Util.ProcessAuthnRequest(HttpContext, out authnRequest, out relayState);

            if (authnRequest == null)
            {
                // No authentication request found.
                goto ReturnView;
            }

            // Process the authentication request.
            bool forceAuthn = authnRequest.ForceAuthn;
            bool allowCreate = false;

            if (authnRequest.NameIdPolicy != null)
            {
                allowCreate = authnRequest.NameIdPolicy.AllowCreate;
            }

            ssoState = new SsoAuthnState();
            ssoState.AuthnRequest = authnRequest;
            ssoState.RelayState = relayState;
            ssoState.IdpProtocolBinding = SamlBindingUri.UriToBinding(authnRequest.ProtocolBinding);
            ssoState.AssertionConsumerServiceURL = authnRequest.AssertionConsumerServiceUrl;

            // Determine whether or not a local login is required.
            bool requireLocalLogin = false;

            if (forceAuthn)
            {
                requireLocalLogin = true;
            }
            else
            {
                if (!User.Identity.IsAuthenticated & allowCreate)
                {
                    requireLocalLogin = true;
                }
            }

            // If a local login is required then save the authentication request 
            // and initiate a local login.
            if (requireLocalLogin)
            {
                // Save the SSO state.
                Session[SsoSessionKey] = ssoState;

                // Initiate a local login.
                System.Web.Security.FormsAuthentication.RedirectToLoginPage();
                goto ReturnView;
            }
        }

        // Create a SAML response with the user's local identity if any.
        ComponentPro.Saml2.Response samlResponse = Util.BuildResponse(HttpContext);

        // Send the SAML response to the service provider.
        Util.SendResponse(HttpContext, samlResponse, ssoState);

        return null;
    }
    catch (Exception exception)
    {
        HttpContext.Trace.Write("IdentityProvider", "An Error occurred", exception);
    }

ReturnView:
    return View();
}

Single Log Out Service

SLO service essentially handles the case that an IdP sends us a request to log out the user from all services. Implementing it is quite straight-forward. We create the SingleLogoutServiceController class to implement single log-out functionality.

public ActionResult Index()
{
    // Receive logout request or response
    X509Certificate2 x509Certificate = (X509Certificate2)HttpContext.Application[Global.SPCertKey];

    LogoutRequest logoutRequest;
    LogoutResponse logoutResponse;
    SamlMessageUtil.CreateLogoutMessage(Request, out logoutResponse, out logoutRequest, x509Certificate.PublicKey.Key);

    if (logoutRequest != null)
        HandleLogoutRequest(logoutRequest, x509Certificate);
    else
        HandleLogoutResponse(logoutResponse);

    return null;
}

void HandleLogoutRequest(LogoutRequest message, X509Certificate2 x509Certificate)
{
    // This is the logged in ID.
    string nameId = message.NameId.NameIdentifier;

    // Do something with the ID, like writing a record about the activity of this user.
    // ...

    // Logout locally.
    AccountController.LogOut();

    #region Create and Send LogoutResponse
    // We need to send back a LogoutResponse to the IdP
    LogoutResponse logoutResponse = new LogoutResponse();
    logoutResponse.Status = new Status(SamlPrimaryStatusCode.Success, null);
    logoutResponse.Issuer = new Issuer(Util.GetAbsoluteUrl(HttpContext, "~/"));

    // Send the logout response.
    logoutResponse.Redirect(Response, WebConfigurationManager.AppSettings["LogoutServiceProviderUrl"], null, x509Certificate.PrivateKey);
    #endregion
}

void HandleLogoutResponse(LogoutResponse message)
{
    SamlTrace.Log(LogLevel.Info, "Received a Logout Response with status code: " + message.Status.StatusCode.ToString());

    // Redirect to the default page.
    Response.Redirect("~/", false);
}

SAML Artifact Resolve

Now we will need to handle the SAML artifact in the SamlArtifactResolveController class.

public ActionResult Index()
{
    try
    {
        // Create an artifact resolve from the request with XML data extracted from the request stream.
        ArtifactResolve artifactResolve = ArtifactResolve.Create(Request);

        // Create the artifact type 0004.
        Saml2ArtifactType0004 httpArtifact = new Saml2ArtifactType0004(artifactResolve.Artifact.ArtifactValue);

        // Remove the artifact state from the cache.
        XmlElement samlResponseXml = (XmlElement)SamlSettings.CacheProvider.Remove(httpArtifact.ToString());

        if (samlResponseXml == null) 
            goto ReturnView;

        // Create an artifact response containing the cached SAML message.
        ArtifactResponse artifactResponse = new ArtifactResponse();
        artifactResponse.Issuer = new Issuer(Util.GetAbsoluteUrl(HttpContext, "~/"));
        artifactResponse.Message = samlResponseXml;

        // Send the artifact response.
        artifactResponse.Send(Response);

        return null;
    }
    catch (Exception exception)
    {
        HttpContext.Trace.Write("ServiceProvider", "An Error occurred", exception);
    }

ReturnView:
    return View();
}

Account Controller

Create Account Controller and implement LogOut() ActionResult to Logout user in a web application.

[Authorize]
public partial class AccountController : Controller
{
    private ApplicationSignInManager _signInManager;
    private ApplicationUserManager _userManager;

    public AccountController()
    {
    }

    public AccountController(ApplicationUserManager userManager, ApplicationSignInManager signInManager)
    {
        UserManager = userManager;
        SignInManager = signInManager;
    }

    public ApplicationSignInManager SignInManager
    {
        get
        {
            return _signInManager ?? HttpContext.GetOwinContext().Get<ApplicationSignInManager>();
        }
        private set
        {
            _signInManager = value;
        }
    }

    public ApplicationUserManager UserManager
    {
        get
        {
            return _userManager ?? HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
        }
        private set
        {
            _userManager = value;
        }
    }

    private void AddErrors(IdentityResult result)
    {
        foreach (var error in result.Errors)
        {
            ModelState.AddModelError("", error);
        }
    }

    #region External Login

    // Used for XSRF protection when adding external logins
    private const string XsrfKey = "XsrfId";

    private IAuthenticationManager AuthenticationManager
    {
        get
        {
            return HttpContext.GetOwinContext().Authentication;
        }
    }

    //
    // POST: /Account/ExternalLogin
    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public ActionResult ExternalLogin(string provider, string returnUrl)
    {
        // Request a redirect to the external login provider
        return new ChallengeResult(provider, Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl }));
    }

    //
    // POST: /Account/ExternalLoginConfirmation
    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl)
    {
        if (User.Identity.IsAuthenticated)
        {
            return RedirectToAction("Index", "Manage");
        }

        if (ModelState.IsValid)
        {
            // Get the information about the user from the external login provider
            var info = await AuthenticationManager.GetExternalLoginInfoAsync();
            if (info == null)
            {
                return View("ExternalLoginFailure");
            }
            var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
            var result = await UserManager.CreateAsync(user);
            if (result.Succeeded)
            {
                result = await UserManager.AddLoginAsync(user.Id, info.Login);
                if (result.Succeeded)
                {
                    await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
                    return RedirectToLocal(returnUrl);
                }
            }
            AddErrors(result);
        }

        ViewBag.ReturnUrl = returnUrl;
        return View(model);
    }

    //
    // GET: /Account/ExternalLoginFailure
    [AllowAnonymous]
    public ActionResult ExternalLoginFailure()
    {
        return View();
    }

    //
    // GET: /Account/ExternalLoginCallback
    [AllowAnonymous]
    public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
    {
        var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
        if (loginInfo == null)
        {
            return RedirectToAction("Login");
        }

        // Sign in the user with this external login provider if the user already has a login
        var result = await SignInManager.ExternalSignInAsync(loginInfo, isPersistent: false);
        switch (result)
        {
            case SignInStatus.Success:
                return RedirectToLocal(returnUrl);
            case SignInStatus.LockedOut:
                return View("Lockout");
            case SignInStatus.RequiresVerification:
                return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = false });
            case SignInStatus.Failure:
            default:
                // If the user does not have an account, then prompt the user to create an account
                ViewBag.ReturnUrl = returnUrl;
                ViewBag.LoginProvider = loginInfo.Login.LoginProvider;
                return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = loginInfo.Email });
        }
    }

    #endregion

    #region Local login

    //
    // GET: /Account/Login
    [AllowAnonymous]
    public ActionResult Login(string returnUrl)
    {
        // If the user is already logged in, redirect to the homepage
        if (HttpContext.User.Identity.IsAuthenticated)
            return RedirectToAction("Index", "Home");

        ViewBag.ReturnUrl = returnUrl;
        return View();
    }

    [HttpPost]
    [AllowAnonymous]
    public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
    {
        if (!ModelState.IsValid)
        {
            return View(model);
        }

        // This doesn't count login failures towards account lockout
        // To enable password failures to trigger account lockout, change to shouldLockout: true
        var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
        switch (result)
        {
            case SignInStatus.Success:
                return RedirectToLocal(returnUrl);
            case SignInStatus.LockedOut:
                return View("Lockout");
            case SignInStatus.RequiresVerification:
                return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
            case SignInStatus.Failure:
            default:
                ModelState.AddModelError("", "Invalid login attempt.");
                return View(model);
        }
    }

    private ActionResult RedirectToLocal(string returnUrl)
    {
        if (Url.IsLocalUrl(returnUrl))
        {
            return Redirect(returnUrl);
        }
        return RedirectToAction("Index", "Home");
    }

    #endregion

    #region Registration

    //
    // GET: /Account/Register
    [AllowAnonymous]
    public ActionResult Register(string returnUrl)
    {
        ViewBag.ReturnUrl = returnUrl;
        return View();
    }

    //
    // POST: /Account/Register
    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> Register(RegisterViewModel model, string returnUrl)
    {
        if (ModelState.IsValid)
        {
            var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
            var result = await UserManager.CreateAsync(user, model.Password);
            if (result.Succeeded)
            {
                // For demonstration purposes only, create some additional claims.
                UserManager.AddClaim(user.Id, new Claim(ClaimTypes.GivenName, model.GivenName));
                UserManager.AddClaim(user.Id, new Claim(ClaimTypes.Surname, model.Surname));

                await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);

                // For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=320771
                // Send an email with this link
                // string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
                // var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
                // await UserManager.SendEmailAsync(user.Id, "Confirm your account", "Please confirm your account by clicking <a href=\"" + callbackUrl + "\">here</a>");

                return RedirectToLocal(returnUrl);
            }
            AddErrors(result);
        }

        // If we got this far, something failed, redisplay form
        return View(model);
    }

    //
    // GET: /Account/ConfirmEmail
    [AllowAnonymous]
    public async Task<ActionResult> ConfirmEmail(string userId, string code)
    {
        if (userId == null || code == null)
        {
            return View("Error");
        }
        var result = await UserManager.ConfirmEmailAsync(userId, code);
        return View(result.Succeeded ? "ConfirmEmail" : "Error");
    }

    //
    // GET: /Account/ForgotPassword
    [AllowAnonymous]
    public ActionResult ForgotPassword()
    {
        return View();
    }

    //
    // POST: /Account/ForgotPassword
    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> ForgotPassword(ForgotPasswordViewModel model)
    {
        if (ModelState.IsValid)
        {
            var user = await UserManager.FindByNameAsync(model.Email);
            if (user == null || !(await UserManager.IsEmailConfirmedAsync(user.Id)))
            {
                // Don't reveal that the user does not exist or is not confirmed
                return View("ForgotPasswordConfirmation");
            }

            // For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=320771
            // Send an email with this link
            // string code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
            // var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);        
            // await UserManager.SendEmailAsync(user.Id, "Reset Password", "Please reset your password by clicking <a href=\"" + callbackUrl + "\">here</a>");
            // return RedirectToAction("ForgotPasswordConfirmation", "Account");
        }

        // If we got this far, something failed, redisplay form
        return View(model);
    }

    //
    // GET: /Account/ForgotPasswordConfirmation
    [AllowAnonymous]
    public ActionResult ForgotPasswordConfirmation()
    {
        return View();
    }

    //
    // GET: /Account/ResetPassword
    [AllowAnonymous]
    public ActionResult ResetPassword(string code)
    {
        return code == null ? View("Error") : View();
    }

    //
    // POST: /Account/ResetPassword
    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> ResetPassword(ResetPasswordViewModel model)
    {
        if (!ModelState.IsValid)
        {
            return View(model);
        }
        var user = await UserManager.FindByNameAsync(model.Email);
        if (user == null)
        {
            // Don't reveal that the user does not exist
            return RedirectToAction("ResetPasswordConfirmation", "Account");
        }
        var result = await UserManager.ResetPasswordAsync(user.Id, model.Code, model.Password);
        if (result.Succeeded)
        {
            return RedirectToAction("ResetPasswordConfirmation", "Account");
        }
        AddErrors(result);
        return View();
    }

    //
    // GET: /Account/ResetPasswordConfirmation
    [AllowAnonymous]
    public ActionResult ResetPasswordConfirmation()
    {
        return View();
    }

    #endregion

    internal static ActionResult LogOut()
    {
        var httpContext = System.Web.HttpContext.Current;
        IOwinContext context = httpContext.Request.GetOwinContext();
        IAuthenticationManager authenticationManager = context.Authentication;
        var authenticationTypes = context.Authentication.GetAuthenticationTypes();
        authenticationManager.SignOut(authenticationTypes.Select(o => o.AuthenticationType).ToArray());

#if IDP
        httpContext.Session.Remove(SingleSignOnServiceController.SsoSessionKey);
#endif

        return new RedirectResult("~/");
    }

    internal static ApplicationUser ResolveAppUser(HttpContextBase context, string userName, IDictionary<string, string> attributes)
    {
        // Automatically provision the user.
        // If the user doesn't exist locally then create the user.
        var applicationUserManager = context.GetOwinContext().Get<ApplicationUserManager>();
        var applicationUser = applicationUserManager.FindByName(userName);

        if (applicationUser == null)
        {
            applicationUser = new ApplicationUser();

            applicationUser.UserName = userName;
            applicationUser.Email = userName;
            applicationUser.EmailConfirmed = true;

            if (attributes != null)
            {
                if (attributes.ContainsKey(ClaimTypes.GivenName))
                {
                    applicationUser.Claims.Add(new IdentityUserClaim() { ClaimType = ClaimTypes.GivenName, ClaimValue = attributes[ClaimTypes.GivenName], UserId = applicationUser.Id });
                }

                if (attributes.ContainsKey(ClaimTypes.Surname))
                {
                    applicationUser.Claims.Add(new IdentityUserClaim() { ClaimType = ClaimTypes.Surname, ClaimValue = attributes[ClaimTypes.Surname], UserId = applicationUser.Id });
                }
            }

            var identityResult = applicationUserManager.Create(applicationUser);

            if (!identityResult.Succeeded)
            {
                throw new Exception(string.Format("The user {0} couldn't be created.\n{1}", userName, identityResult));
            }
        }

        return applicationUser;
    }

    // **************************************
    // URL: /Account/LogOff
    // **************************************

    public ActionResult LogOff()
    {
        LogOut();

        // Create a logout request.
        LogoutRequest logoutRequest = new LogoutRequest();
        logoutRequest.Issuer = new Issuer(Util.GetAbsoluteUrl(HttpContext, "~/"));
        logoutRequest.NameId = new NameId(HttpContext.User.Identity.Name);

        // Send the logout request to the SP over HTTP redirect.
#if IDP
        string logoutUrl = WebConfigurationManager.AppSettings["LogoutServiceProviderUrl"];
#else
        string logoutUrl = WebConfigurationManager.AppSettings["LogoutIdProviderUrl"];
#endif
        X509Certificate2 x509Certificate = (X509Certificate2)HttpContext.Application[Global.IdPCertKey];

        logoutRequest.Redirect(Response, logoutUrl, logoutUrl, x509Certificate.PrivateKey);            

        return null;
    }

    internal class ChallengeResult : HttpUnauthorizedResult
    {
        public ChallengeResult(string provider, string redirectUri)
            : this(provider, redirectUri, null)
        {
        }

        public ChallengeResult(string provider, string redirectUri, string userId)
        {
            LoginProvider = provider;
            RedirectUri = redirectUri;
            UserId = userId;
        }

        public string LoginProvider { get; set; }
        public string RedirectUri { get; set; }
        public string UserId { get; set; }

        public override void ExecuteResult(ControllerContext context)
        {
            var properties = new AuthenticationProperties { RedirectUri = RedirectUri };
            if (UserId != null)
            {
                properties.Dictionary[XsrfKey] = UserId;
            }
            context.HttpContext.GetOwinContext().Authentication.Challenge(properties, LoginProvider);
        }
    }
}

45-Day Money Back Guarantee

We will refund your full money in 45 days
if you are not satisfied with our products

Buy Now

Dont miss out Get update on new articles and other opportunities