aspnet / AspNetKatana

Microsoft's OWIN implementation, the Katana project
Apache License 2.0
959 stars 331 forks source link

App redirects to different Auth Type refresh token URL. #512

Closed smileyiori closed 7 months ago

smileyiori commented 10 months ago

Hi, currently I have a ASP.NET MVC 5 application where it is using two type of authentication (AD B2C and AAD). In this app, authentication and authorization work well except when it comes with refresh token. I have identified that the last registered authentication type is setting the default configuration for refresh token. For example, if I register AD B2C first and after AAD, when AD B2C tries to refresh token, it redirects to AAD instead of AD B2C and vice versa.

FYI: If I only have AD B2C registered, the refresh token goes to AD B2C and gets refreshed.

Here is my code:

public partial class Startup
{
        public void ConfigureAuth(IAppBuilder app)
        {
            AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;

            app.SetDefaultSignInAsAuthenticationType("ADB2CAuthentication");
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = "ADB2CAuthentication", 
                LoginPath = new PathString("/Login/ADB2C"),
                CookieName = "ADB2CAuth.B2CCookie",
            });

            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = "AADAuthentication",
                LoginPath = new PathString("/Login/AAD"),
                CookieName = "AAD.AzureADCookie",
            });

            ConfigureADB2C(app);
            ConfigureAAD(app);
        }

        private void ConfigureADB2C(IAppBuilder app)
        {
            var op = GetADB2COptions();
            app.UseOpenIdConnectAuthentication(options);
        }

        private void ConfigureAAD(IAppBuilder app)
        {
            var op = GetAADOptions();
            app.UseOpenIdConnectAuthentication(options);
        } 

// Here starts AD B2C configuration

       public OpenIdConnectAuthenticationOptions GetADB2COptions()
        {
            string tenantName = "MyTenant";
            string clientId = "MyClientId";
            string clientSecret = "MyClientSecret";
            string redirectUri = "https://localhost:44300/";
            string signInPolicy = "MyPolicy";

            string authority = string.Format("https://{0}.b2clogin.com/tfp/{1}/{2}",
                tenantName,
                $"{tenantName}.onmicrosoft.com",
                signInPolicy);

            string[] supportedScopes = new string[]
            {
                "openid",
                "profile",
                "offline_access",
                $"https://{tenantName}.onmicrosoft.com/api/read",
                $"https://{tenantName}.onmicrosoft.com/api/write",
            };

            return new OpenIdConnectAuthenticationOptions("ADB2CAuthentication")
            {
                MetadataAddress = $"{authority}/v2.0/.well-known/openid-configuration",

                ClientId = clientId,
                RedirectUri = redirectUri,
                PostLogoutRedirectUri = redirectUri,
                Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    RedirectToIdentityProvider = CreateRedirectToIdentityProviderHandler(signInPolicy),
                    AuthorizationCodeReceived = CreateAuthorizationCodeReceivedHandler(authority, clientId, clientSecret, redirectUri, supportedScopes),
                    AuthenticationFailed = CreateAuthenticationFailedHandler()
                },

                TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = ClaimTypes.Name,
                    ValidAudience = clientId
                },
                SignInAsAuthenticationType = "ADB2CAuthentication",

                Scope = string.Join(" ", supportedScopes)
            };
        }

        private Func<RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>, Task> CreateRedirectToIdentityProviderHandler(
            string signInPolicy)
        {
            return (notification) =>
            {
                string alternatePolicyId = notification.OwinContext.Get<string>("MyAlternatePolicy");

                if (!string.IsNullOrEmpty(alternatePolicyId) && !alternatePolicyId.Equals(signInPolicy))
                {
                    notification.ProtocolMessage.Scope = OpenIdConnectScope.OpenId;
                    notification.ProtocolMessage.ResponseType = OpenIdConnectResponseType.IdToken;
                    notification.ProtocolMessage.IssuerAddress = notification.ProtocolMessage.IssuerAddress.ToLower().Replace(signInPolicy.ToLower(), alternatePolicyId.ToLower());
                }

                return Task.FromResult(0);
            };
        }

        private Func<AuthorizationCodeReceivedNotification, Task> CreateAuthorizationCodeReceivedHandler(
            string b2cAuthority,
            string clientId,
            string clientSecret,
            string redirectUri,
            string[] scopes)
        {
            return async (notification) =>
            {
                notification.HandleCodeRedemption();

                IConfidentialClientApplication cca = ConfidentialClientApplicationBuilder.Create(clientId)
                    .WithB2CAuthority(b2cAuthority)
                    .WithRedirectUri(redirectUri)
                    .WithClientSecret(clientSecret)
                    .Build();

                string objectIdClaimValue = notification
                    .AuthenticationTicket
                    .Identity
                    .FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")
                    .Value;

                //Save cache using Redis
                //var tokenCache = new MyTokenKen();
                //tokenCache.EnableSerialization(cca.UserTokenCache);

                AuthenticationResult result = await cca.AcquireTokenByAuthorizationCode(scopes, notification.Code)
                .ExecuteAsync();

                notification.HandleCodeRedemption(null, result.IdToken);
            };
        }

        private Func<AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>, Task> CreateAuthenticationFailedHandler()
        {
            return (notification) =>
            {
                notification.HandleResponse();

                string error = notification.ProtocolMessage.ErrorDescription?.ToLower();

                if (error != null && error.Contains(OpenIdConnectErrorCode.UserRequestedPasswordReset))
                {
                    notification.Response.Redirect("/Login/ResetPassword");
                }
                else if (error != null && error.Contains(OpenIdConnectErrorCode.UserCancelledPasswordReset))
                {
                    notification.Response.Redirect("/Login/ADB2C");
                }
                else
                {
                    throw new AuthenticationException("ADB2CAuthentication", $"OpenID Connect authentication failure when trying to authenticate with ADB2C tenant.", notification.Exception);
                }

                return Task.FromResult(0);
            };
        }

   // Here starts AAD configuration

  public OpenIdConnectAuthenticationOptions GetAADOptions()
        {
            string clientId = "MyClientId";
            string clientSecret = "MyClientSecret";
            string redirectUri = "";

            return new OpenIdConnectAuthenticationOptions("AADAuthentication")
            {
                ClientId = clientId,
                Authority = "https://login.microsoftonline.com/organizations/v2.0",
                Scope = $"openid email profile offline_access",
                RedirectUri = redirectUri,
                PostLogoutRedirectUri = redirectUri,
                TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = ClaimTypes.Name,
                    IssuerValidator = (issuer, token, tvp) =>
                    {
                       return issuer;
                    }
                },
                SignInAsAuthenticationType = "AADAuthentication",
                Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    AuthenticationFailed = CreateAuthenticationFailedHandler(),
                    AuthorizationCodeReceived = CreateAuthorizationCodeReceivedHandler(clientId, clientSecret, redirectUri),
                }
            };
        }

        private Func<AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>, Task> CreateAuthenticationFailedHandler()
        {
            return (notification) =>
            {
                notification.HandleResponse();

                string error = notification.ProtocolMessage.ErrorDescription?.ToLower();

                if (error != null && error.Contains(OpenIdConnectErrorCode.UserNotAssignedToApplication))
                {
                    notification.Response.Redirect($"Error/Forbidden");
                }
                else if (notification.Exception is SecurityTokenInvalidIssuerException)
                {
                    notification.Response.Redirect($"Error/Forbidden");
                }
                else
                {
                    throw new AuthenticationException("AADAuthentication", $"OpenID Connect failure,", notification.Exception);
                }

                return Task.FromResult(0);
            };
        }

        private Func<AuthorizationCodeReceivedNotification, Task> CreateAuthorizationCodeReceivedHandler(
         string appId,
         string appSecret,
         string redirectUri)
        {
            return async (notification) =>
            {
                notification.HandleCodeRedemption();

                IConfidentialClientApplication cca = ConfidentialClientApplicationBuilder.Create(appId)
                    .WithRedirectUri(redirectUri)
                    .WithClientSecret(appSecret)
                    .Build();

                var signedInUser = new ClaimsPrincipal(notification.AuthenticationTicket.Identity);
                string uniqueUser = GetUsersUniqueId(signedInUser);

                //Save cache using Redis
                //var tokenCache = new MyTokenKen();
                //tokenCache.EnableSerialization(cca.UserTokenCache);

                AuthenticationResult result = await cca.AcquireTokenByAuthorizationCode(null, notification.Code).ExecuteAsync();

                notification.HandleCodeRedemption(null, result.IdToken);
            };
        }

        private string GetUsersUniqueId(ClaimsPrincipal user)
        {
            // Combine the user's object ID with their tenant ID

            if (user != null)
            {
                string userObjectId = user.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value ??
                                      user.FindFirst("oid").Value;

                string userTenantId = user.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value ??
                                      user.FindFirst("tid").Value;

                if (!string.IsNullOrEmpty(userObjectId) && !string.IsNullOrEmpty(userTenantId))
                {
                    return $"{userObjectId}.{userTenantId}";
                }
            }

            return null;
        }
}

In the Controller, I am using this to authenticate:

HttpContextExtensions.GetOwinContext(HttpContext)
.Authentication.Challenge(new AuthenticationProperties() { RedirectUri = "/" }, "ADB2CAuthentication");

Am I doing anything wrong on this? Is something related to the cookies?

Tratcher commented 10 months ago

I don't follow, UseOpenIdConnectAuthentication doesn't support refresh tokens, that must be handled at a higher layer.

smileyiori commented 10 months ago

Hi @Tratcher thanks for answering. My question is, if I only use Azure AD B2C I can see the token being refreshed, because it goes to the right URL to get a new token. But you mentioned that should be handled at higher layer, how would I do that?

Tratcher commented 9 months ago

Azure AD B2C is the higher layer I'm talking about, that's extended functionality beyond what's offered by the Microsoft.Owin components here. You need to ask AzureAd about their components.

Tratcher commented 7 months ago

Closing as external