Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug] Azure function appears 'broken' in portal #916

Closed
8 tasks
smkanadl opened this issue Feb 1, 2021 · 11 comments
Closed
8 tasks

[Bug] Azure function appears 'broken' in portal #916

smkanadl opened this issue Feb 1, 2021 · 11 comments

Comments

@smkanadl
Copy link

smkanadl commented Feb 1, 2021

Which version of Microsoft Identity Web are you using?
Note that to get help, you need to run the latest version.

Where is the issue?

  • Web app
    • Sign-in users
    • Sign-in users and call web APIs
  • Web API
    • Protected web APIs (validating tokens)
    • Protected web APIs (validating scopes)
    • Protected web APIs call downstream web APIs
  • Token cache serialization
    • In-memory caches
    • Session caches
    • Distributed caches
  • Other (please describe)
    An Azure function validating tokens in http triggers using the supplied example

This is a new app or an experiment.

Repro

Expected behavior
Function is up and running after deployment.

Actual behavior

  • The deployment process using func azure functionapppublish yields Error calling sync triggers (BadRequest)
  • The http triggers seem to work (I can call them e.g. using curl)
  • If I try to monitor the function in the Azure portal however, I notice some more strange behaviour:
    • Accessing the log stream does not work: Unable to fetch the host status of your function app. To use log streaming, please make sure your function host is running.
    • Calling the triggers does not work. They won't even show up in the Function overview menu point. It just says: Azure Functions Runtime is unreachable.
    • I've crawled through all the log files and EventLog but no error or exception was found
  • As soon as I remove the AddAuthentication setup in the startup,

image
image

@jmprieur
Copy link
Collaborator

jmprieur commented Feb 1, 2021

@maliksahil : would you be able to help here?
thanks!

@jmprieur
Copy link
Collaborator

jmprieur commented Feb 1, 2021

@smkanadl
Copy link
Author

smkanadl commented Feb 1, 2021

@jmprieur Yes, I've read his blog post. His example code and the official test code hosted here are identical (at least I've seen no difference).

@Valks
Copy link

Valks commented Feb 25, 2021

When you call AddAuthentication you override the default auth providers in Azure functions (Arm, ScriptAuthLevel, JwtBearer) which naturally causes havoc with the Azure backend.

see: Azure/azure-functions-host#6805

I've been trying to use reflection to restore those providers.

From early testing the code below seems to work.
(Small Note: the Debug flags are there for local development only. Locally it puts the auth handlers through CliAuthenticationHandler, that said the Arm and ScriptAuthLevel seem to still fail)

  • To Use (Adjust to suit)
    Startup.cs
public override void Configure(IFunctionsHostBuilder builder)
{
    // Authentication Provider
    builder.Services.AddAuthentication(sharedOptions =>
    {
        // Assigning the DefaultScheme also creates problems.
        //sharedOptions.DefaultScheme = Microsoft.Identity.Web.Constants.Bearer;
        //sharedOptions.DefaultChallengeScheme = Microsoft.Identity.Web.Constants.Bearer;
    })
        .AddArmToken()
        .AddScriptAuthLevel()
        //.AddScriptJwtBearer();
        .AddMicrosoftIdentityWebApi(Configuration.GetSection(Microsoft.Identity.Web.Constants.AzureAdB2C))
        .EnableTokenAcquisitionToCallDownstreamApi()
        //.AddDownstreamWebApi("", Configuration.GetSection(""))
        .AddInMemoryTokenCaches();

    services.AddAuthorization(options =>
    {
        options.AddScriptPolicies();
    });

    services.AddAuthLevelAuthorizationHandler()
        .AddNamedAuthLevelAuthorizationHandler()
        .AddFunctionAuthorizationHandler();
}
  • Extension Methods
public static class AuthenticationStartupExtensions
    {
#if DEBUG
        private const string CliAuthenticationHandler = "Azure.Functions.Cli.Actions.HostActions.WebHost.Security.CliAuthenticationHandler`1";
        private static Type? _cliAuthenticationHandlerType = null;
        private static Type CliAuthenticationHandlerType
        {
            get
            {
                if (_cliAuthenticationHandlerType is null)
                {
                    _cliAuthenticationHandlerType = AppDomain.CurrentDomain
                        .GetAssemblies()
                        .SelectMany(x => x.GetTypes())
                        .FirstOrDefault(x => x.IsClass && x.FullName == CliAuthenticationHandler);
                }
                return _cliAuthenticationHandlerType;
            }
        }
#endif
        // ARM Authentication
#if DEBUG
        private const string ArmAuthenticationOptions = "Microsoft.Azure.WebJobs.Script.WebHost.Security.Authentication.ArmAuthenticationOptions";
        private static Type? _armAuthenticationOptionsType = null;
        private static Type ArmAuthenticationOptionsType
        {
            get
            {
                if (_armAuthenticationOptionsType is null)
                {
                    _armAuthenticationOptionsType = AppDomain.CurrentDomain
                        .GetAssemblies()
                        .SelectMany(x => x.GetTypes())
                        .FirstOrDefault(x => x.IsClass && x.FullName == ArmAuthenticationOptions);
                }
                return _armAuthenticationOptionsType;
            }
        }

        public static AuthenticationBuilder AddArmToken(this AuthenticationBuilder builder)
        {
                var method = builder.GetType().GetMethods()
                    .Where(x => x.Name == nameof(AuthenticationBuilder.AddScheme))
                    .FirstOrDefault(x => x.GetParameters().Length == 2);
                var genericMethod = method.MakeGenericMethod(ArmAuthenticationOptionsType, CliAuthenticationHandlerType.MakeGenericType(ArmAuthenticationOptionsType));
                return genericMethod.Invoke(builder, new object[] { "ArmToken", new Action<dynamic>((_) => { }) }) as AuthenticationBuilder;
        }
#else
        private const string ArmAuthenticationExtensions = "Microsoft.Extensions.DependencyInjection.ArmAuthenticationExtensions";
        private static Type? _armAuthenticationExtensionsTypes = null;
        private static Type ArmAuthenticationExtensionsType
        {
            get
            {
                if (_armAuthenticationExtensionsTypes is null)
                {
                    _armAuthenticationExtensionsTypes = AppDomain.CurrentDomain
                        .GetAssemblies()
                        .SelectMany(x => x.GetTypes())
                        .FirstOrDefault(x => x.IsClass && x.FullName == ArmAuthenticationExtensions);
                }
                return _armAuthenticationExtensionsTypes;
            }
        }

        public static AuthenticationBuilder AddArmToken(this AuthenticationBuilder builder)
        {
            var method = ArmAuthenticationExtensionsType.GetMethods()
                .Where(x => x.Name == nameof(AddArmToken))
                .FirstOrDefault(x => x.GetParameters().Length == 1);
            return method.Invoke(null, new object[] { builder }) as AuthenticationBuilder;
        }
#endif
        // Script Authentication Level
#if DEBUG
        private const string AuthenticationLevelOptions = "Microsoft.Azure.WebJobs.Script.WebHost.Authentication.AuthenticationLevelOptions";
        private static Type? _authenticationLevelOptionsType = null;
        private static Type AuthenticationLevelOptionsType
        {
            get
            {
                if(_authenticationLevelOptionsType is null)
                {
                    _authenticationLevelOptionsType = AppDomain.CurrentDomain
                        .GetAssemblies()
                        .SelectMany(x => x.GetTypes())
                        .FirstOrDefault(x => x.IsClass && x.FullName == AuthenticationLevelOptions);
                }
                return _authenticationLevelOptionsType;
            }
        }

        public static AuthenticationBuilder AddScriptAuthLevel(this AuthenticationBuilder builder)
        {
            var method = builder.GetType().GetMethods()
                .Where(x => x.Name == nameof(AuthenticationBuilder.AddScheme))
                .FirstOrDefault(x => x.GetParameters().Length == 2);
            var genericMethod = method.MakeGenericMethod(AuthenticationLevelOptionsType, CliAuthenticationHandlerType.MakeGenericType(AuthenticationLevelOptionsType));
            return genericMethod.Invoke(builder, new object[] { "WebJobsAuthLevel", new Action<dynamic>((_) => { }) }) as AuthenticationBuilder;
        }
#else
        private const string AuthLevelExtensionsExtensions = "Microsoft.Extensions.DependencyInjection.AuthLevelExtensions";
        private static Type? _authLevelExtensionsExtensionsTypes = null;
        private static Type AuthLevelExtensionsExtensionsType
        {
            get
            {
                if (_authLevelExtensionsExtensionsTypes is null)
                {
                    _authLevelExtensionsExtensionsTypes = AppDomain.CurrentDomain
                        .GetAssemblies()
                        .SelectMany(x => x.GetTypes())
                        .FirstOrDefault(x => x.IsClass && x.FullName == AuthLevelExtensionsExtensions);
                }
                return _authLevelExtensionsExtensionsTypes;
            }
        }

        public static AuthenticationBuilder AddScriptAuthLevel(this AuthenticationBuilder builder)
        {
            var method = AuthLevelExtensionsExtensionsType.GetMethods()
                .Where(x => x.Name == nameof(AddScriptAuthLevel))
                .FirstOrDefault(x => x.GetParameters().Length == 1);
            return method.Invoke(null, new object[] { builder }) as AuthenticationBuilder;
        }
#endif
        // JWT Bearer

        private const string JwtBearerExtensions = "Microsoft.Extensions.DependencyInjection.ScriptJwtBearerExtensions";
        private static Type? _jwtBearerExtensionsTypes = null;
        private static Type JwtBearerExtensionsType
        {
            get
            {
                if (_jwtBearerExtensionsTypes is null)
                {
                    _jwtBearerExtensionsTypes = AppDomain.CurrentDomain
                        .GetAssemblies()
                        .SelectMany(x => x.GetTypes())
                        .FirstOrDefault(x => x.IsClass && x.FullName == JwtBearerExtensions);
                }
                return _jwtBearerExtensionsTypes;
            }
        }

        public static AuthenticationBuilder AddScriptJwtBearer(this AuthenticationBuilder builder)
        {
            var method = JwtBearerExtensionsType.GetMethods()
                .Where(x => x.Name == nameof(AddScriptJwtBearer))
                .FirstOrDefault(x => x.GetParameters().Length == 1);
            return method.Invoke(null, new object[] { builder }) as AuthenticationBuilder;
        }
    }
    public static class AuthorizationStartupExtensions
    {
        private const string AuthorizationOptionsExtensions = "Microsoft.Azure.WebJobs.Script.WebHost.Security.Authorization.Policies.AuthorizationOptionsExtensions";
        private static Type? _authorizationOptionsExtensionsType = null;
        private static Type AuthorizationOptionsExtensionsType
        {
            get
            {
                if (_authorizationOptionsExtensionsType is null)
                {
                    _authorizationOptionsExtensionsType = AppDomain.CurrentDomain
                        .GetAssemblies()
                        .SelectMany(t => t.GetTypes())
                        .FirstOrDefault(t => t.IsClass && t.FullName == AuthorizationOptionsExtensions);
                }
                return _authorizationOptionsExtensionsType;
            }
        }

        public static void AddScriptPolicies(this AuthorizationOptions options)
        {
            var method = AuthorizationOptionsExtensionsType.GetMethods()
                .Where(x => x.Name == nameof(AddScriptPolicies))
                .FirstOrDefault(x => x.GetParameters().Length == 1);
            method.Invoke(null, new object[] { options });
        }

        private const string AuthLevelAuthorizationHandler = "Microsoft.Azure.WebJobs.Script.WebHost.Security.Authorization.AuthLevelAuthorizationHandler";
        private static Type? _authLevelAuthorizationHandlerType = null;
        private static Type AuthLevelAuthorizationHandlerType
        {
            get
            {
                if (_authLevelAuthorizationHandlerType is null)
                {
                    _authLevelAuthorizationHandlerType = AppDomain.CurrentDomain
                        .GetAssemblies()
                        .SelectMany(t => t.GetTypes())
                        .FirstOrDefault(t => t.IsClass && t.FullName == AuthLevelAuthorizationHandler);
                }
                return _authLevelAuthorizationHandlerType;
            }
        }

        public static IServiceCollection AddAuthLevelAuthorizationHandler(this IServiceCollection services)
        {
            var method = typeof(ServiceCollectionServiceExtensions).GetMethods()
                .Where(x => x.Name == nameof(ServiceCollectionServiceExtensions.AddSingleton))
                .FirstOrDefault(x => x.GetParameters().Length == 1);
            var genericMethod = method.MakeGenericMethod(typeof(IAuthorizationHandler), AuthLevelAuthorizationHandlerType);
            return genericMethod.Invoke(null, new object[] { services }) as IServiceCollection;
        }

        private const string NamedAuthLevelAuthorizationHandler = "Microsoft.Azure.WebJobs.Script.WebHost.Security.Authorization.NamedAuthLevelAuthorizationHandler";
        private static Type? _namedAuthLevelAuthorizationHandlerType = null;
        private static Type NamedAuthLevelAuthorizationHandlerType
        {
            get
            {
                if (_namedAuthLevelAuthorizationHandlerType is null)
                {
                    _namedAuthLevelAuthorizationHandlerType = AppDomain.CurrentDomain
                        .GetAssemblies()
                        .SelectMany(t => t.GetTypes())
                        .FirstOrDefault(t => t.IsClass && t.FullName == NamedAuthLevelAuthorizationHandler);
                }
                return _namedAuthLevelAuthorizationHandlerType;
            }
        }

        public static IServiceCollection AddNamedAuthLevelAuthorizationHandler(this IServiceCollection services)
        {
            var method = typeof(ServiceCollectionServiceExtensions).GetMethods()
                .Where(x => x.Name == nameof(ServiceCollectionServiceExtensions.AddSingleton))
                .FirstOrDefault(x => x.GetParameters().Length == 1);
            var genericMethod = method.MakeGenericMethod(typeof(IAuthorizationHandler), NamedAuthLevelAuthorizationHandlerType);
            return genericMethod.Invoke(null, new object[] { services }) as IServiceCollection;
        }

        private const string FunctionAuthorizationHandler = "Microsoft.Azure.WebJobs.Script.WebHost.Security.Authorization.FunctionAuthorizationHandler";
        private static Type? _functionAuthorizationHandlerType = null;
        private static Type FunctionAuthorizationHandlerType
        {
            get
            {
                if (_functionAuthorizationHandlerType is null)
                {
                    _functionAuthorizationHandlerType = AppDomain.CurrentDomain
                        .GetAssemblies()
                        .SelectMany(t => t.GetTypes())
                        .FirstOrDefault(t => t.IsClass && t.FullName == FunctionAuthorizationHandler);
                }
                return _functionAuthorizationHandlerType;
            }
        }

        public static IServiceCollection AddFunctionAuthorizationHandler(this IServiceCollection services)
        {
            var method = typeof(ServiceCollectionServiceExtensions).GetMethods()
                .Where(x => x.Name == nameof(ServiceCollectionServiceExtensions.AddSingleton))
                .FirstOrDefault(x => x.GetParameters().Length == 1);
            var genericMethod = method.MakeGenericMethod(typeof(IAuthorizationHandler), FunctionAuthorizationHandlerType);
            return genericMethod.Invoke(null, new object[] { services }) as IServiceCollection;
        }
    }

@hajekj
Copy link
Contributor

hajekj commented Mar 13, 2021

I have been trying to look into this, since we are hitting it too... I have hard time understanding at which point the ConfigureServices method gets overridden. Custom startup gets called via ConfigureStartup in WebJobs-Host, but from my tests, calling AddAuthentication twice doesn't seem to have any side-effect. So that would mean that it doesn't get called at all or is discarded somewhere?

/cc: @Valks

@Valks
Copy link

Valks commented Mar 13, 2021

@hajekj It's been two weeks since I looked at this but my recollection is when you call AddAuthentication it replaces the auth providers already registered which is what's happening to the providers registered in ConfigureServices

https://github.com/Azure/azure-functions-host/blob/1f1103d2f6be8b16a70ea9367f2611fa68e7a4c7/src/WebJobs.Script.WebHost/WebHostServiceCollectionExtensions.cs#L48

https://github.com/Azure/azure-functions-host/blob/1f1103d2f6be8b16a70ea9367f2611fa68e7a4c7/src/WebJobs.Script.WebHost/WebHostServiceCollectionExtensions.cs#L46

If you register a provider yourself and then go AddAuthentication a second time it should remove any providers you registered the first time you call AddAuthentication.

If you go through the code I provided above you can use reflection to re-add the functions providers when you register your custom provider restoring the original functionality along with your custom provider.

I remember also having an issue when setting the default scheme, you have to leave it blank i.e. don't set it to Bearer otherwise it won't resolve the provider properly. That said, it may have been an issue I had when configuring Finbuckle but worth noting if you run into issues.

@hajekj
Copy link
Contributor

hajekj commented Mar 13, 2021

@Valks Thanks for the insight!

I wasn't able to check if the Function providers get registered before or after (it would make sense if they got registered before), but then, the implementation of AddAuthentication doesn't seem to override anything in there - that's why I would like to understand the pipeline of service configuration.

I actually tried doing this:

services.AddAuthentication().AddJwtBearer("Bearer1", ...);
services.AddAuthentication().AddJwtBearer("Bearer2", ...);

and unless I missed something, I was able to authenticate with both schemes, which would support, that it doesn't get overridden (at least in this case).

I suppose it all comes to the fact that there is something different going on with the ServiceProvider as it is being passed through the custom Startup.

Update: Also wanted to add that your solution seems to be so far the only thing which fixes the behavior. Thanks!

@jmprieur
Copy link
Collaborator

@hajekj : on the second line, you should pass-in a different authentication scheme to AddAuthentication, otherwise this will map to the default authorization scheme:

services.AddAuthentication().AddJwtBearer("Bearer1", ...);
services.AddAuthentication("Bearer2").AddJwtBearer("Bearer2", ...);

@hajekj
Copy link
Contributor

hajekj commented Mar 15, 2021

@jmprieur: Yes, I am aware of that, I am just trying to understand what happens in the Function's runtime that causes the pre-registered schemes like (ArmToken, ...) to be "unregistered", therefor causing the issue described above.

@hajekj
Copy link
Contributor

hajekj commented Mar 25, 2021

I will try to ask some related questions to this at the Functions Live event today - https://www.youtube.com/watch?v=4n0QbG0wYEI

@jennyf19
Copy link
Collaborator

closing, as Azure Functions with .NET 8 is working. Please re-open if that's not the case

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Development

No branches or pull requests

5 participants