14
votes

I'm trying to implement Firebase 3 Authentication mechanism using Custom Tokens (as described at https:// firebase.google.com/docs/auth/server/create-custom-tokens).

My server is ASP.NET MVC Application.

So according to the instructions (https://firebase.google.com/docs/server/setup) I've created a service account for my Firebase application and generated a key in '.p12' format.

After that according to instructions here (https://firebase.google.com/docs/auth/server/create-custom-tokens#create_custom_tokens_using_a_third-party_jwt_library) I tried to generate a custom token and sign it using the key received on the previous step. For token generation I used SystemIdentityModel.Tokens.Jwt library from Microsoft, so the code looks like the following:

var now = DateTime.UtcNow;
var tokenHandler = new JwtSecurityTokenHandler();
var key = new X509AsymmetricSecurityKey(new X509Certificate2(p12path, p12pwd));
var signinCredentials = new SigningCredentials(key, "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", "http://www.w3.org/2001/04/xmlenc#rsa-sha256");
Int32 nowInUnixTimestamp = (Int32)(now.Subtract(new DateTime(1970, 1, 1))).TotalSeconds;

var token = tokenHandler.CreateToken(
            issuer: serviceAccountEmail,
            audience: "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",                
            signingCredentials: signinCredentials,
            subject: new ClaimsIdentity(new Claim[]
                    {
                    new Claim("sub", serviceAccountEmail),
                    new Claim("iat", nowInUnixTimestamp.ToString()),
                    new Claim("exp", (nowInUnixTimestamp + (60*60)).ToString()),
                    new Claim("uid", uid)
                    })
            );

var tokenString = tokenHandler.WriteToken(token);

Then tried to sign in user in React Native application using Firebase Javascript SDK, with the following code:

//omitting initialization code
firebase.auth().signInWithCustomToken(firebaseJWT).catch(function(error) {
            console.log('Error authenticating Firebase user. Code: ' + error.code + ' Message: ' + error.message);            
        });

But got an error from Firebase saying:

Error authenticating Firebase user. Code: auth/invalid-custom-token Message: The custom token format is incorrect. Please check the documentation.

Experimenting with adding different claims for token expiration control didn't help either.

Also I tried to generate tokens with "dvsekhvalnov/jose-jwt" library but can't get it working with "RS256" algorithm.

So the question:

Any suggestion on what am I doing wrong?

4
I realized that the token format described by the link stackoverflow.com/questions/37408684/… is a token issued by Firebase itself, so the first question is not a question anymore.Ilya Zatolokin
Here is the answer from the Google support on the same question: "I saw your post in SO, that you already had a workaround. For the token format, you should always follow what's on the latest documentation. There are some issues with regard to authentication right now and we're doing our best to keep things moving on our end. Keep an eye out on our release notes for any further updates, and feel free to check back with us if needed." So looks like workaround is the best option for now.Ilya Zatolokin

4 Answers

15
votes

This pure .NET solution works for me, using the Org.BouncyCastle (https://www.nuget.org/packages/BouncyCastle/) and Jose.JWT (https://www.nuget.org/packages/jose-jwt/) libraries.

I followed these steps:

  • In the Firebase console click the 'cog' icon which is top left, next to the project name, and click 'Permissions'.
  • At the IAM and Admin page, click 'Service Accounts' on the left
  • Click 'Create Service Account' at the top, enter a 'Service Account Name', select 'Project->Editor' in the Role selection, tick the 'Furnish a new private key' checkbox and select JSON
  • Click 'Create' and download the Service Account JSON file and keep it safe.
  • Open the Service Account JSON file in a suitable text editor and put the values into the following code:

    // private_key from the Service Account JSON file
    public static string firebasePrivateKey=@"-----BEGIN PRIVATE KEY-----\nMIIE...\n...\n...\n...\n...\n...\n...\n...\n...\n...\n...\n...\n...\n...\n...\n...\n...\n...\n...\n...\n...\n...\n...\n...\n...\n...\n-----END PRIVATE KEY-----\n";
    
    // Same for everyone
    public static string firebasePayloadAUD="https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit";
    
    // client_email from the Service Account JSON file
    public static string firebasePayloadISS="[email protected]";
    public static string firebasePayloadSUB="[email protected]";
    
    // the token 'exp' - max 3600 seconds - see https://firebase.google.com/docs/auth/server/create-custom-tokens
    public static int firebaseTokenExpirySecs=3600;
    
    private static RsaPrivateCrtKeyParameters _rsaParams;
    private static object _rsaParamsLocker=new object();
    
    void Main() {
        // Example with custom claims
        var uid="myuserid";
        var claims=new Dictionary<string, object> {
            {"premium_account", true}
        };
        Console.WriteLine(EncodeToken(uid, claims));
    }
    
    public static string EncodeToken(string uid, Dictionary<string, object> claims) {
        // Get the RsaPrivateCrtKeyParameters if we haven't already determined them
        if (_rsaParams == null) {
            lock (_rsaParamsLocker) {
                if (_rsaParams == null) {
                    StreamReader sr = new StreamReader(GenerateStreamFromString(firebasePrivateKey.Replace(@"\n","\n")));
                    var pr = new Org.BouncyCastle.OpenSsl.PemReader(sr);
                    _rsaParams = (RsaPrivateCrtKeyParameters)pr.ReadObject();
                }
            }
        }
    
        var payload = new Dictionary<string, object> {
            {"claims", claims}
            ,{"uid", uid}
            ,{"iat", secondsSinceEpoch(DateTime.UtcNow)}
            ,{"exp", secondsSinceEpoch(DateTime.UtcNow.AddSeconds(firebaseTokenExpirySecs))}
            ,{"aud", firebasePayloadAUD}
            ,{"iss", firebasePayloadISS}
            ,{"sub", firebasePayloadSUB}
        };
    
        return Jose.JWT.Encode(payload, Org.BouncyCastle.Security.DotNetUtilities.ToRSA(_rsaParams), JwsAlgorithm.RS256);
    }
    
    private static long secondsSinceEpoch(DateTime dt) {
        TimeSpan t = dt - new DateTime(1970, 1, 1);
        return (long)t.TotalSeconds;
    }
    
    private static Stream GenerateStreamFromString(string s) {
        MemoryStream stream = new MemoryStream();
        StreamWriter writer = new StreamWriter(stream);
        writer.Write(s);
        writer.Flush();
        stream.Position = 0;
        return stream;
    }
    

To get this working in IIS I needed to change the application's pool identity and set the "load user profile" setting to true.

2
votes

Haven't found a direct answer for the question so far, so for now ended up with the following solution:

Using instruction here generated a JSON file with service account details and created a basic Node.js server using Firebase server SDK that does generate correct custom tokens for Firebase with the following code:

var http = require('http');
var httpdispatcher = require('httpdispatcher');
var firebase = require('firebase');

var config = {
    serviceAccount: {
    projectId: "{projectId}",
    clientEmail: "{projectServiceEmail}",
    privateKey: "-----BEGIN PRIVATE KEY----- ... ---END PRIVATE KEY-----\n"
  },
  databaseURL: "https://{projectId}.firebaseio.com"
};

firebase.initializeApp(config);    

const PORT=8080; 

httpdispatcher.onGet("/firebaseCustomToken", function(req, res) {
    var uid = req.params.uid;

    if (uid) {
        var customToken = firebase.auth().createCustomToken(uid);
        res.writeHead(200, {'Content-Type': 'application/json'});
        res.end(JSON.stringify({'firebaseJWT' : customToken}));
    } else {
        res.writeHead(400, {'Content-Type': 'text/plain'});
        res.end('No uid parameter specified');
    }
});    

function handleRequest(request, response){
     try {
        //log the request on console
        console.log(request.url);
        //Disptach
        httpdispatcher.dispatch(request, response);
    } catch(err) {
        console.log(err);
    }    
}

//create a server
var server = http.createServer(handleRequest);

//start our server
server.listen(PORT, function(){       
    console.log("Server listening on: http://localhost:%s", PORT);
});

Maybe someone will find this helpful.

2
votes

@Elliveny's answer worked great for me. I am using it in a .NET Core 2.0 application and have built upon the accepted answer to turn this solution into a class that can be registered as a singleton dependency in the app services container, as well as have configuration passed in via constructor so that we can leverage .NET secrets for local development configuration and environment variables for production configuration.

I have also tidied up the stream handling a bit.

Note for .NET Core devs - you'll need to use Portable.BouncyCastle

You can test your encoded results by parsing the output JWT token with Jwt.IO

using Jose;
using Org.BouncyCastle.Crypto.Parameters;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

public class FirebaseTokenGenerator
{
    // private_key from the Service Account JSON file
    public static string firebasePrivateKey;

    // Same for everyone
    public static string firebasePayloadAUD = "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit";

    // client_email from the Service Account JSON file
    public static string firebasePayloadISS;
    public static string firebasePayloadSUB;

    // the token 'exp' - max 3600 seconds - see https://firebase.google.com/docs/auth/server/create-custom-tokens
    public static int firebaseTokenExpirySecs = 3600;

    private static RsaPrivateCrtKeyParameters _rsaParams;
    private static object _rsaParamsLocker = new object();

    public FirebaseTokenGenerator(string privateKey, string clientEmail)
    {
        firebasePrivateKey = privateKey ?? throw new ArgumentNullException(nameof(privateKey));
        firebasePayloadISS = clientEmail ?? throw new ArgumentNullException(nameof(clientEmail));
        firebasePayloadSUB = clientEmail;
    }

    public static string EncodeToken(string uid)
    {
        return EncodeToken(uid, null);
    }

    public static string EncodeToken(string uid, Dictionary<string, object> claims)
    {
        // Get the RsaPrivateCrtKeyParameters if we haven't already determined them
        if (_rsaParams == null)
        {
            lock (_rsaParamsLocker)
            {
                if (_rsaParams == null)
                {
                    using (var streamWriter = WriteToStreamWithString(firebasePrivateKey.Replace(@"\n", "\n")))
                    {
                        using (var sr = new StreamReader(streamWriter.BaseStream))
                        {
                            var pr = new Org.BouncyCastle.OpenSsl.PemReader(sr);
                            _rsaParams = (RsaPrivateCrtKeyParameters)pr.ReadObject();
                        }
                    }
                }
            }
        }

        var payload = new Dictionary<string, object> {
        {"uid", uid}
        ,{"iat", SecondsSinceEpoch(DateTime.UtcNow)}
        ,{"exp", SecondsSinceEpoch(DateTime.UtcNow.AddSeconds(firebaseTokenExpirySecs))}
        ,{"aud", firebasePayloadAUD}
        ,{"iss", firebasePayloadISS}
        ,{"sub", firebasePayloadSUB}
    };

        if (claims != null && claims.Any())
        {
            payload.Add("claims", claims);
        }

        return JWT.Encode(payload, Org.BouncyCastle.Security.DotNetUtilities.ToRSA(_rsaParams), JwsAlgorithm.RS256);
    }


    private static long SecondsSinceEpoch(DateTime dt)
    {
        TimeSpan t = dt - new DateTime(1970, 1, 1);
        return (long) t.TotalSeconds;
    }

    private static StreamWriter WriteToStreamWithString(string s)
    {
        MemoryStream stream = new MemoryStream();
        StreamWriter writer = new StreamWriter(stream);
        writer.Write(s);
        writer.Flush();
        stream.Position = 0;
        return writer;
    }
}
2
votes

The @Elliveny's code worked for me in locally but in azure throws an error : "The system cannot find the file specified". Due that I have changed a little bit the code and now works in both servers.

private string EncodeToken(string uid, Dictionary<string, object> claims)
    {

        string jwt = string.Empty;
        RsaPrivateCrtKeyParameters _rsaParams;

        using (StreamReader sr = new StreamReader(GenerateStreamFromString(private_key.Replace(@"\n", "\n"))))
        {
            var pr = new Org.BouncyCastle.OpenSsl.PemReader(sr);
            _rsaParams = (RsaPrivateCrtKeyParameters)pr.ReadObject();
        }


        using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
        {
            Dictionary<string, object> payload = new Dictionary<string, object> {
                {"claims", claims}
                ,{"uid", uid}
                ,{"iat", secondsSinceEpoch(DateTime.UtcNow)}
                ,{"exp", secondsSinceEpoch(DateTime.UtcNow.AddSeconds(firebaseTokenExpirySecs))}
                ,{"aud", firebasePayloadAUD}
                ,{"iss", client_email}
                ,{"sub", client_email}
            };

            RSAParameters rsaParams = DotNetUtilities.ToRSAParameters(_rsaParams);
            rsa.ImportParameters(rsaParams);
            jwt = JWT.Encode(payload, rsa, Jose.JwsAlgorithm.RS256);
        }

        return jwt;

    }