Search code examples
authenticationdocusignapiclaims-based-identity

Can I Combine Identities from two Authentication Schemes in ASP.NET Core 3?


I have a web application that utilizes our organization's CAS servers for authentication. I am currently developing an integration with DocuSign for this application. A user will first come to our site and sign in with CAS. Then, they can go to the DocuSign area of the application, sign in to DocuSign, and then perform some functions tied to the DocuSign API. Each of these pieces works properly within my application.

My current problem is that I cannot access both Identities within the HttpContext simultaneously. I need the CAS Identity to determine user behavior and access to certain functions. I need the DocuSign Identity to get the necessary values to enable the API calls. In Startup.cs

    services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
       .AddCookie(options =>
       {
          options.LoginPath = new PathString("/Account/Login");
       })
       .AddCAS(options =>
       {
           options.CasServerUrlBase = Configuration["CasBaseUrl"];
           options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
       })
       .AddOAuth("DocuSign", options ->
       {
          //necessary DocuSign options
       });

I have a HomeController that uses the Authorize attribute and properly requires CAS to access. I can properly access the claims from the CAS Identity. I have a DocusignController where I use Authorize(AuthenticationSchemes = "DocuSign"). An ActionFilter that applies to this controller shows that the DocuSign Identity is coming through properly. Unfortunately, the CAS Identity is not attached, so I cannot check for things such as admin permissions. I examined the cookies, and saw that the .AspNetCore.Cookies value changes when I go between the different Authentication Schemes.

One attempt at a solution was to change the DocusignController to have Authorize(AuthenticationSchemes = "DocuSign, CAS" as its attribute. This seems to be an 'or' rather than an 'and' of the two schemes. If CAS was the most recent scheme, then the CAS Identity is the one seen in my ActionFilter. Same for DocuSign.

Another attempt was to create my own AuthenticationHandler to combine the two Identities. Appended to the AddAuthentication above:

    .AddScheme<CombinedSchemeOptions, CombinedHandler>
        ("Combined", op => { });

Then, within this CombinedHandler:

    protected async override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var CasResult = await Context.AuthenticateAsync("CAS");
        if(CasResult?.Principal != null)
        {
            principal.AddIdentities(CasResult.Principal.Identities);
        }
        var DSResult = await Context.AuthenticateAsync("DocuSign");
        if(DSResult?.Principal != null)
        {
            principal.AddIdentities(DSResult.Principal.Identities);
        }
        var ticket = new AuthenticationTicket(principal, "Combined");
        return AuthenticateResult.Success(ticket);
    }

This seems to behave the same way as before. The most recent Scheme I've used, upon entering the Combined scheme, will give a successful Result with an Identity, and the other Result will return with a failed result.

Is it possible for me to have these two Identities within the HttpContext.User simultaneously? Is it otherwise possible for me to combine claims from the two Identities into one and pass that along?


Solution

  • Further research and tinkering has led me to a solution. First, I discovered that I needed the two authentication schemes to write to separate cookies. I then made sure the DocuSign scheme used the new cookie as its SignInScheme.

    .AddCookie("DSCookie")
    .AddOAuth("DocuSign", options =>
    {
       options.SignInScheme = "DSCookie";
       //further DocuSign options
    }
    

    I also learned that the Authorize tag works as an OR statement (if you have CAS OR DocuSign, you are authorized for this action), yet it will pass along all valid identities permitted by the scheme (You have CAS and you have DocuSign? Bring both Identities in with you). Thus, the second part of my solution has a single action that requires DocuSign authentication, prompting the necessary challenge. Then, I move to actions that have both authentication schemes, and both Identities are brought along properly.

    EDIT: To elaborate per Dmitriy's request

    [Authorize(AuthenticationSchemes = "DocuSign")]
    public ActionResult Index()
    {
       return RedirectToAction("Control");
    }
    
    [Authorize(AuthenticationSchemes = "DocuSign,CAS")]
    public ActionResult Control()
    {
       //relevant code
    }
    

    I click on a link to go to the Index action. Since I currently do not have a valid DocuSign Identity, it performs the necessary prompt and takes me to the sign in screen. If I linked directly to Control, Authentication would see that I currently had a valid CAS Identity, so would approve my access, even though I did not yet have the DocuSign Identity.

    After the sign in, when I arrive at the Index function, only the DocuSign Identity is currently available. I then redirect to Control. That function verifies I have at least one of the specified Identities, grants me access, and passes along any of the listed Identities I currently possess. Thus, inside Control, I can grab information from both CAS and DocuSign.