Search code examples
asp.net-mvcazureazure-active-directorysingle-sign-onazure-ad-msal

Why is a client secret required when setting up an Azure AD / Identity SSO in Asp.net (legacy .Net Framework), but it is not required in asp.net core?


When following the "quick start" guides in the app registration section in the Entra Portal (https://entra.microsoft.com/) I noticed a key difference.

It appears that, when I download and try to run the sample project for Asp.net (legacy), I get an error saying that a client secret is required. However, the documentation and that README included in the solution do not mention that a client secret is a required step.

In contrast, the similar sample project for asp.net core, works without a client secret.

Why does this difference exist? Am I missing something?

What governs that the app is a "Confidential Client"?

The help link in the error goes here -> https://aka.ms/msal-net-client-credentials

Exception Message

I don't understand how or why my asp.net core apps can work without a client secret. But for the legacy asp.net apps, it is seemingly required. I would like to configure my app registrations consistently if possible. Also, if it turns out, it is best practice to have a client secret anyway for asp.net core apps, I will do that. My other concern is that I would have to worry about them expiring.

Note: I am only using the Azure service for authentication in a server side web app. I am not using graph api or anything else.

Edit:

I noticed a key difference in the example code on the website vs the downloaded sample project:

Sample Project -> Startup.cs

public void Configuration(IAppBuilder app)
{
        app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

        app.UseCookieAuthentication(new CookieAuthenticationOptions());
        OwinTokenAcquirerFactory factory = TokenAcquirerFactory.GetDefaultInstance<OwinTokenAcquirerFactory>();

        app.AddMicrosoftIdentityWebApp(factory);
        factory.Services
            .Configure<ConfidentialClientApplicationOptions>(options => { options.RedirectUri = "https://localhost:44368/"; })
            .AddMicrosoftGraph()
            .AddInMemoryTokenCaches();
        factory.Build();
}

Example on Entra Portal Quickstart guide website -> startup.cs

public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
    new OpenIdConnectAuthenticationOptions
    {
        // Sets the client ID, authority, and redirect URI as obtained from Web.config
        ClientId = clientId,
        Authority = authority,
        RedirectUri = redirectUri,
        // PostLogoutRedirectUri is the page that users will be redirected to after sign-out. In this case, it's using the home page
        PostLogoutRedirectUri = redirectUri,
        Scope = OpenIdConnectScope.OpenIdProfile,
        // ResponseType is set to request the code id_token, which contains basic information about the signed-in user
        ResponseType = OpenIdConnectResponseType.CodeIdToken,
        // ValidateIssuer set to false to allow personal and work accounts from any organization to sign in to your application
        // To only allow users from a single organization, set ValidateIssuer to true and the 'tenant' setting in Web.> config to the tenant name
        // To allow users from only a list of specific organizations, set ValidateIssuer to true and use the ValidIssuers parameter
        TokenValidationParameters = new TokenValidationParameters()
        {
            ValidateIssuer = false // Simplification (see note below)
        },
        // OpenIdConnectAuthenticationNotifications configures OWIN to send notification of failed authentications to > the OnAuthenticationFailed method
        Notifications = new OpenIdConnectAuthenticationNotifications
        {
            AuthenticationFailed = OnAuthenticationFailed
        }
    }
);
}

If I then update the code to follow the pattern on the quickstart guide, I can get the desired result and a client secret is not required to authenticate.

Note: Authority must be a URL with the tenant ID, such as:

"https://login.microsoftonline.com/{tenantID}/v2.0"

Now I am wondering, why is there this difference between the downloaded sample and the quickstart explanation, and which is the recommended approach for legacy asp.net mvc apps?

Also it appears that, the part of the sample code that uses Microsoft Graph no longer works with this approach. This is not going to be an issue for me, but I tried to otherwise get Graph working too but I couldnt figure it out.

I even tried doing a hybrid approach, but this generated the same error about requiring a client secret for a confidential client:

OwinTokenAcquirerFactory factory = TokenAcquirerFactory.GetDefaultInstance<OwinTokenAcquirerFactory>();

        app.AddMicrosoftIdentityWebApp(factory);
        factory.Services
            .Configure<OpenIdConnectAuthenticationOptions>(options => {
                // Sets the client ID, authority, and redirect URI as obtained from Web.config
                options.ClientId = "{clientID}";
                options.Authority = "https://login.microsoftonline.com/{tenantID}/v2.0";
                options.RedirectUri = "https://localhost:44368/";
                // PostLogoutRedirectUri is the page that users will be redirected to after sign-out. In this case, it's using the home page
                options.PostLogoutRedirectUri = "https://localhost:44368/";
                options.Scope = OpenIdConnectScope.OpenIdProfile;
                // ResponseType is set to request the code id_token, which contains basic information about the signed-in user
                options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
                // ValidateIssuer set to false to allow personal and work accounts from any organization to sign in to your application
                // To only allow users from a single organization, set ValidateIssuer to true and the 'tenant' setting in Web.> config to the tenant name
                // To allow users from only a list of specific organizations, set ValidateIssuer to true and use the ValidIssuers parameter
                options.TokenValidationParameters = new TokenValidationParameters()
                {
                    ValidateIssuer = false // Simplification (see note below)
                };
                // OpenIdConnectAuthenticationNotifications configures OWIN to send notification of failed authentications to > the OnAuthenticationFailed method
                options.Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    AuthenticationFailed = OnAuthenticationFailed
                };
            })
            .AddMicrosoftGraph()
            .AddInMemoryTokenCaches();
        factory.Build();

Solution

  • Both the codes (Legacy ASP.NET) are correct to Authenticate the App, both use different protocols and Middleware.

    • The code which is downloaded from Entra ID contains the configuration for MicrosoftGraph and in-memory token caching as well.
    .AddMicrosoftGraph()
    .AddInMemoryTokenCaches();
    
    • As it is configured using ConfidentialClientApplicationOptions, Client Secret is must here.You can see the below code by right click => Go to Definition on ConfidentialClientApplicationOptions.

    enter image description here

      public class ConfidentialClientApplicationOptions : ApplicationOptions
      {     
          public string ClientSecret { get; set; }
      }
    

    All confidential clients have a choice of using client secrets or certificate credentials.

    • Also refer the SOThread which explains the option to use without Client Secret.

    .NET Core App is also configured with OpenIdConnect, you can also use Client Secret here.

    My other concern is that I would have to worry about them expiring.

    You can create a new ClientSecret if the old one expires.

    Refer Monitor Azure App Registration Client Secret Expiration for more details.

    which is the recommended approach for legacy asp.net mvc apps?

    • It is better to use OIDC flow which allows to work without Client Secrets avoiding risk of expiring and exposing them. Also, the flow relies on the browser to handle the Authentication.