AzureAD / microsoft-authentication-library-for-dotnet

Microsoft Authentication Library (MSAL) for .NET
https://aka.ms/msal-net
MIT License
1.39k stars 340 forks source link

Protected Web API example using MSAL .NET (.NET 4.7 and not with .NET CORE) #2180

Closed jrmcdona closed 3 years ago

jrmcdona commented 3 years ago

I cannot seem to find a Protect Web API example using .NET.

I have an MSAL.JS 2 Angular app and I have a .NET Web API in which I need to validate the Bearer token.

Does this exsit?

Thanks

jmprieur commented 3 years ago

@jrmcdona : Did you look at this one: https://github.com/Azure-Samples/ms-identity-javascript-angular-spa-aspnetcore-webapi

Other protected web API samples are referenced here: https://docs.microsoft.com/en-us/azure/active-directory/develop/sample-v2-code#web-apis. You might want to look at https://github.com/Azure-Samples/active-directory-dotnet-native-aspnetcore-v2/tree/master/1.%20Desktop%20app%20calls%20Web%20API

jrmcdona commented 3 years ago

@jmprieur those are ,NET Core. I am looking for .NET 4.7 or so. I have a working project with .NET Core but right now I have a first party app and I am trying to move to MSAL so I can get rid of RPS. But I have yet to find a clear example.

jmprieur commented 3 years ago

@jrmcdona : indeed, I'm not sure we have a good .NET Framework example for web APIs. Good point; Note that for the token cache serialization, we just released yesterday a version of MIcrosoft.Identity.Web which provides some implementation for .NET Framework: See https://github.com/AzureAD/microsoft-identity-web/wiki/asp-net#token-cache-serialization-for-msalnet

jmprieur commented 3 years ago

@jrmcdona : did you look at this sample: https://github.com/Azure-Samples/ms-identity-aspnet-webapi-onbehalfof ?

jrmcdona commented 3 years ago

@jmprieur that sample uses WindowsAzureActiveDirectoryBearerAuthenticationOptions and I was told to use UseJwtBearerAuthentication. So I have been trying to make it work with Jwt. Do you know for sure which I should use?

Mine is a protected Web API and not a on behalf of downstream API.

     app.UseJwtBearerAuthentication(
              new JwtBearerAuthenticationOptions
jmprieur commented 3 years ago

@jrmcdona : the todolist service is a protected API (which moreover calls a downstream API). But I just found this one: https://github.com/Azure-Samples/active-directory-dotnet-webapi-getting-started. the code is ASP.NET Core, but the readme seems to explain how to proceed for .NET Fw: https://github.com/Azure-Samples/active-directory-dotnet-webapi-getting-started#about-the-basic-backend-service

jmprieur commented 3 years ago

@kalyankrishna1 : do we have a better sample of protected web API for ASP.NET FW?

jrmcdona commented 3 years ago

@kalyankrishna1 @jmprieur Since I am trying to use MSAL.NET should I need ne using client applications to validate the token?

https://identitydocs.azurewebsites.net/static/v2/msal-net-initializing-client-applications.html

jmprieur commented 3 years ago

@jrmcdona : no the tokens need to be validated with Middleware. It does not need to use MSAL.NET until you want to acquire a token for a downstream API. I forgot to ask.: did you see https://docs.microsoft.com/azure/active-directory/develop/scenario-protected-web-api-overview ?

shridharkurhade commented 1 year ago

I am a facing similar issue for configuring Azure Ad/Protecting our custom Web API. I have angularjs in the front end and ASP.Net 4.8 in the backend. Could you please provide a working example? I tried different combinations from the Azure sample directory like JavaScript samples as AngularJS MSAL libary is discontinue and I also tried the samples suggested above. But none of them seems to be working.

How can I protect my web api with Azure AD configuration in ASP.Net 4.8?

bgavrilMS commented 1 year ago

@shridharkurhade - there is a sample dedicated to ASP.NET classic - https://learn.microsoft.com/en-us/azure/active-directory/develop/sample-v2-code#web-applications - does this not work for you?

shridharkurhade commented 1 year ago

Hi @bgavrilMS ,

The classic ASP.Net works when I have the whole application in ASP.Net e.g. if I use open id configuration and have Sign-in , sign-out calls in ASP.Net application.

But I want to trigger sign-in, sign-out and refresh token to Azure AD from AngularJS code and my web API code is in ASP.Net.

Could you please suggest an example for this combination? How I can configure Azure AD configuration in my ASP.Net with additional claims and retrieve it in AngularJS.

There are examples of ASP.Net core and Angular (not AngularJS), which I can find. In these examples, I can see that MSAL.Net and MSAL Angular used for configuration but didn't find any working example of Classic ASP.Net and AngujarJS.

jmprieur commented 1 year ago

@kalyankrishna1 is it something you can help with?

k290 commented 2 months ago

@bgavrilMS @kalyankrishna1 this is something we also are looking to do as part of our migration to modernising our massive legacy web app.

Edit: I've figured it out. Will post the solution here later today or tomorrow to help anyone for the future

k290 commented 2 months ago

@bgavrilMS @jmprieur The solution is as follows:

There's two aspects I wasn't entirely certain of which I point out below and still need to research. The code works though so its 99% of the job done.

Startup.Auth.cs

      ServicePointManager.Expect100Continue = true;
      ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls
                                             | SecurityProtocolType.Tls11
                                             | SecurityProtocolType.Tls12
                                             | SecurityProtocolType.Ssl3;

 var authority = $"https://login.microsoftonline.com/{Config.TenantId}/v2.0";
   var discoveryDoc = $"{authority}/.well-known/openid-configuration";

   var keyResolver = new OpenIdV2ConnectSigningKeyResolver(discoveryDoc);
   var tokenParams = new TokenValidationParameters
   {

       AuthenticationType = OAuthDefaults.AuthenticationType,
       ValidAudiences = new[] { Config.ApiClientIdId, $"api://{Config.ApiClientIdId}" },
       ValidateIssuer = true,
       ValidIssuer = authority,
       IssuerSigningKeyResolver = (token, securityToken, kid, parameters) => keyResolver.GetSigningKey(kid)

   };

   ep.UseJwtBearerAuthentication(new JwtBearerAuthenticationOptions
   {
       AuthenticationMode = AuthenticationMode.Active,
       IssuerSecurityKeyProviders = new[] { new OpenIdConnectV2SecurityKeyProvider(discoveryDoc) },
       TokenValidationParameters = tokenParams
   });

Not entirely sure if authenticationmode.Active is necessary. Then in the above you'll see that I had to implement both the OpenIdConnectV2SecurityKeyProvider and OpenIdV2ConnectSigningKeyResolver.

Here are the implementations of each:

OpenIdV2ConnectSigningKeyResolver Not entirely sure if I should use a ReaderWriterLockSlim similar to the Provider . (i.e. is it a critical section??)


       //This is necessary because when using .NET Framework UseJwtBearer doesn't know what V2.0 endpoint to call to validate signing keys.
    public class OpenIdV2ConnectSigningKeyResolver
    {
        private readonly OpenIdConnectConfiguration openIdConfig;

        public OpenIdV2ConnectSigningKeyResolver(string metadataEndpoint)
        {
            var cm = new ConfigurationManager<OpenIdConnectConfiguration>(metadataEndpoint, new OpenIdConnectConfigurationRetriever());
            var configRequest = cm.GetConfigurationAsync().ConfigureAwait(false); //Always use ConfigureAwait(false) to avoid deadlocks when running async code unavoidably in sync method
            this.openIdConfig = configRequest.GetAwaiter().GetResult();
        }

        public SecurityKey[] GetSigningKey(string kid)
        {
            return new[] { this.openIdConfig.JsonWebKeySet.GetSigningKeys().FirstOrDefault(t => t.KeyId == kid) };
        }
    }
`KeyProvider.cs`

     //This class is necessary because the OAuthBearer Middleware does not leverage
 // the OpenID Connect metadata endpoint exposed by the V2 STS by default.

 public class OpenIdConnectV2SecurityKeyProvider : IIssuerSecurityKeyProvider
 {
     public ConfigurationManager<OpenIdConnectConfiguration> ConfigManager;
     private string _issuer;
     private IEnumerable<SecurityKey> _keys;
     private readonly string _metadataEndpoint;

     private readonly ReaderWriterLockSlim _synclock = new ReaderWriterLockSlim();

     public OpenIdConnectSecurityKeyProvider(string metadataEndpoint)
     {
         _metadataEndpoint = metadataEndpoint;
         ConfigManager = new ConfigurationManager<OpenIdConnectConfiguration>(metadataEndpoint, new OpenIdConnectConfigurationRetriever());

         RetrieveMetadata();
     }

     /// <summary>
     /// Gets the issuer the credentials are for.
     /// </summary>
     /// <value>
     /// The issuer the credentials are for.
     /// </value>
     public string Issuer
     {
         get
         {
             RetrieveMetadata();
             _synclock.EnterReadLock();
             try
             {
                 return _issuer;
             }
             finally
             {
                 _synclock.ExitReadLock();
             }
         }
     }

     public IEnumerable<SecurityKey> SecurityKeys
     {
         get
         {
             RetrieveMetadata();
             _synclock.EnterReadLock();
             try
             {
                 return _keys;
             }
             finally
             {
                 _synclock.ExitReadLock();
             }
         }
     }

     private void RetrieveMetadata()
     {
         _synclock.EnterWriteLock();
         try
         {
             OpenIdConnectConfiguration config = ConfigManager.GetConfigurationAsync().Result;
             _issuer = config.Issuer;
             _keys = config.SigningKeys;
         }
         finally
         {
             _synclock.ExitWriteLock();
         }
     }
 }

Assuming you have split your Azure app registrations with an API app reg and a SPA app reg, you need to make sure that the API app reg has api: {requestedAccessTokenVersion: 2} (new manifest) or accessTokenAcceptedVersion: 2 (old soon to be deprecated AAD Graph manifest) set in the manifest. One of ours had null which made the app still try to use the V1 authority