firebase / firebase-admin-dotnet

Firebase Admin .NET SDK
https://firebase.google.com/docs/admin/setup
Apache License 2.0
366 stars 131 forks source link

Unable to authenticate using firebase-cli approach instead of service-account.json credentials #292

Closed floppydisken closed 3 years ago

floppydisken commented 3 years ago

Environment description

Steps to reproduce:

I'm trying to authenticate the FirebaseAdmin calls using the firebase-tools access token. To do this I'm loading the configstore firebase-tools.json file and reading the access token. Then using the project-id returned by firebase use --non-interactive to tell FirebaseAdmin which project I want to operate on. However this results in an authentication error and I'm not sure why it fails.

  1. Login to firebase with firebase login.
  2. Choose project to operate on using firebase use <project-id>
  3. Run the following code:

    namespace Utils {
    public class Shell
    {
        public static string Run(string command)
        {
            var process = new Process()
            {
                StartInfo = new ProcessStartInfo()
                {
                    FileName = "/bin/bash",
                    Arguments = $@"-c ""{command}""",
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    RedirectStandardInput = true,
                }
            };
    
            process.Start();
            StreamReader sr = process.StandardOutput;
            var result = sr.ReadToEnd();
            process.WaitForExit();
    
            return result;
        }
    }
    
    public class FirebaseUtils
    {
        public class EnvironmentVariableNotFoundException : Exception
        {
            public EnvironmentVariableNotFoundException(string variableName) : base($"Variable not initialized or unavailable: {variableName}")
            { }
        }
    
        public static GoogleCredential LoadGoogleCredentials()
        {
            return GoogleCredential.FromAccessToken(GetAccessTokenFromConfigStore());
        }
    
        public static string GetProjectIdFromFirebaseCli()
            => Shell.Run("npx firebase use --non-interactive").Trim('\n');
    
        private static string GetFirebaseConfigPath()
        {
            var homeDirectory = Environment.GetEnvironmentVariable("HOME")
                ?? Environment.GetEnvironmentVariable("HOMEPATH")
                ?? Environment.GetEnvironmentVariable("HOMEDRIVE")
                ?? throw new EnvironmentVariableNotFoundException("HOME | HOMEPATH | HOMEDRIVE");
    
            return $"{homeDirectory}/.config/configstore/firebase-tools.json";
        }
    
        private static bool FirebaseConfigExists()
            => File.Exists(GetFirebaseConfigPath());
    
        private static bool IsLoggedInToFirebase()
            => !string.IsNullOrWhiteSpace(GetAccessTokenFromConfigStore());
    
        private static string GetAccessTokenFromConfigStore()
        {
            if (!FirebaseConfigExists())
                throw new FileNotFoundException(
                    "Missing configuration created with firebase tools. Aborting. Use firebase-tools to login.");
    
            var firebaseConfigLocation = GetFirebaseConfigPath();
            var firebaseConfigAsRawJson = File.ReadAllText(firebaseConfigLocation);
    
            var accessTokenObject = new { tokens = new { access_token = "", id_token = "" } };
            var deserializedFirebaseConfig = JsonConvert.DeserializeAnonymousType(
                firebaseConfigAsRawJson, 
                accessTokenObject
            );
            var accessToken = accessTokenObject.tokens.access_token;
    
            return accessToken;
        }
    }
    }

    And use it like so

    ...
    public class Program
    {
        public static int Main(string[] args)
        {
            FirebaseApp.Create(new AppOptions { Credential = FirebaseUtils.LoadGoogleCredentials() });
            var users = FirebaseAuth.DefaultInstance.ListUsersAsync().Result;
        }
    }
    ...

    Throws

at FirebaseAdmin.Util.ErrorHandlingHttpClient`1.<SendAndReadAsync>d__10.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
at FirebaseAdmin.Util.ErrorHandlingHttpClient`1.<SendAndDeserializeAsync>d__7`1.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
at FirebaseAdmin.Util.AdaptedListResourcesRequest`2.<SendAndDeserializeAsync>d__3`1.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() 
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) 
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
at FirebaseAdmin.Auth.Users.ListUsersRequest.<ExecuteAsync>d__10.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)   
at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
at Google.Api.Gax.Rest.ResponseAsyncEnumerable`3.ResponseAsyncEnumerator.<MoveNextAsync>d__9.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at Google.Api.Gax.Rest.ResourceEnumerator`3.<MoveNextAsync>d__11.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
Unexpected HTTP response with status: 401 (Unauthorized)
{
  "error": {
    "code": 401,
    "message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
    "errors": [
      {
        "message": "Invalid Credentials",
        "domain": "global",
        "reason": "authError",
        "location": "Authorization",
        "locationType": "header"
      }
    ],
    "status": "UNAUTHENTICATED"
  }
}

I thought I was clever integrating the authentication with firebase-tools, but it turns out it doesn't work. I want to use firebase-tools to easily switch between projects. In particular I have a development firebase-project for messing about and a production project for the serious stuff. This is to serve as an alternative to creating service-credentials-picker-logic. I do not really understand why I'm getting this error. It's as if the credentials are not applied to the request if the GoogleCredential is generated from an access token.

Can anyone shed some light on why this is failing?

hiranya911 commented 3 years ago

Note: In the future please use StackOverflow for API usage and debugging questions.

There are couple of issues in the above code. First, the following deserialization logic doesn't seem to work:

            var accessTokenObject = new { tokens = new { access_token = "", id_token = "" } };
            var deserializedFirebaseConfig = JsonConvert.DeserializeAnonymousType(
                firebaseConfigAsRawJson, 
                accessTokenObject
            );
            var accessToken = accessTokenObject.tokens.access_token;

I had to read the acccess_token from deserializedFirebaseConfig instead.

Secondly, the project ID is required configuration for accessing Auth APIs. So you must pass it explicitly via AppOptions.

Once I fixed those issues, I was able to observe the same error as you. But this is only because the access_token written to the Firebase config file was expired (it's only valid for an hour). So I ran firebase logout followed by firebase login to force a new token to be written to the config file. Then my test code (shown below) ran without any issues:

        static void Main(string[] args)
        {
            FirebaseApp.Create(new AppOptions {
                Credential = FirebaseUtils.LoadGoogleCredentials(),
                ProjectId = MyProjectId,
            });
            var users = FirebaseAuth.DefaultInstance.GetUserAsync(MyUid).Result;
            Console.WriteLine(users.Email);
        }
floppydisken commented 3 years ago

Thank you for clarifying. I actually thought there was an issue with the library, since it seemed strange to me that I could access my projects through the firebase-tools with what seemed like the access token provided by the config, hence why I posted the issue here. I'll pass it by stackoverflow first next time. The logout and login works like a charm.