web-dev-qa-db-de.com

Firebase 3: Erstellen eines benutzerdefinierten Authentifizierungstokens mithilfe von .net und c #

Ich versuche, den Firebase 3-Authentifizierungsmechanismus mithilfe von benutzerdefinierten Token zu implementieren (wie unter https://freeebase.google.com/docs/auth/server/create-custom-tokens beschrieben).

Mein Server ist ASP.NET MVC Application.

Entsprechend den Anweisungen ( https://firebase.google.com/docs/server/setup ) habe ich ein Dienstkonto für meine Firebase-Anwendung erstellt und einen Schlüssel im Format ".p12" generiert.

Danach wurde gemäß den Anweisungen hier ( https://firebase.google.com/docs/auth/server/create-custom-tokens#create_custom_tokens_using_a_third-party_jwt_library ) versucht, ein benutzerdefiniertes Token zu erstellen und es mit dem Schlüssel zu signieren im vorherigen Schritt erhalten. Für die Token-Generierung habe ich die Bibliothek SystemIdentityModel.Tokens.Jwt von Microsoft verwendet, daher sieht der Code folgendermaßen aus:

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);

Dann wurde versucht, den Benutzer in der React Native-Anwendung mit dem Firebase Javascript SDK mit folgendem Code anzumelden:

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

Bekam einen Fehler von Firebase und sagte:

Fehler beim Authentifizieren des Firebase-Benutzers. Code: auth/invalid-custom-token Nachricht: Das Format des benutzerdefinierten Tokens ist falsch. Bitte überprüfen Sie die Dokumentation.

Das Experimentieren mit dem Hinzufügen verschiedener Ansprüche für die Kontrolle des Tokenablaufs hat auch nicht geholfen.

Ich habe auch versucht, Token mit der Bibliothek "dvsekhvalnov/jose-jwt" zu generieren, kann aber nicht mit dem Algorithmus "RS256" arbeiten.

Also die Frage:

Irgendwelche Vorschläge, was mache ich falsch?

13
Ilya Zatolokin

Diese reine .NET-Lösung funktioniert für mich mit Org.BouncyCastle ( https://www.nuget.org/packages/BouncyCastle/ ) und Jose.JWT ( https://www.nuget.org)/packages/jose-jwt/ ) Bibliotheken.

Ich bin diesen Schritten gefolgt:

  • Klicken Sie in der Firebase-Konsole auf das "Zahnrad" -Symbol oben links neben dem Projektnamen und klicken Sie auf "Berechtigungen".
  • Klicken Sie auf der IAM- und Admin-Seite links auf "Dienstkonten"
  • Klicken Sie oben auf "Service-Account erstellen", geben Sie einen "Service-Account-Namen" ein, wählen Sie in der Rollenauswahl "Projekt-> Editor" aus, aktivieren Sie das Kontrollkästchen "Neuen privaten Schlüssel einrichten" und wählen Sie "JSON"
  • Klicken Sie auf "Erstellen", laden Sie die JSON-Datei des Dienstkontos herunter und bewahren Sie sie auf.
  • Öffnen Sie die JSON-Datei des Dienstkontos in einem geeigneten Texteditor und geben Sie die Werte in den folgenden Code ein:

    // private_key from the Service Account JSON file
    public static string [email protected]"-----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;
    }
    

Damit dies in IIS funktioniert, musste ich die Poolidentität der Anwendung ändern und die Einstellung "Benutzerprofil laden" auf "true" setzen. 

14
Elliveny

Habe bis jetzt keine direkte Antwort auf die Frage gefunden, also endete es mit der folgenden Lösung:

Unter Verwendung der Anweisung hier wurde eine JSON-Datei mit Dienstkontodetails generiert und ein grundlegender Node.js-Server mit Firebase-Server-SDK erstellt, der korrekte benutzerdefinierte Token für Firebase mit dem folgenden Code generiert:

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);
});

Vielleicht findet jemand das hilfreich.

2
Ilya Zatolokin

Der @ Elliveny-Code hat lokal für mich funktioniert, aber in Azure wird ein Fehler ausgegeben: "Das System kann die angegebene Datei nicht finden". Aufgrund dessen habe ich den Code ein wenig geändert und funktioniert jetzt auf beiden Servern.

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;

    }
2
Pau Faner Canet

@ Ellivenys Antwort hat für mich gut funktioniert. Ich verwende es in einer .NET Core 2.0-Anwendung und habe auf der akzeptierten Antwort aufgebaut, um diese Lösung in eine Klasse umzuwandeln, die als Einzelabhängigkeit im App-Services-Container registriert werden kann und die Konfiguration über den Konstruktor übergeben wird Wir können .NET-Geheimnisse für die lokale Entwicklungskonfiguration und Umgebungsvariablen für die Produktionskonfiguration nutzen.

Ich habe auch das Stream-Handling etwas aufgeräumt.

Hinweis für .NET Core-Entwickler - Sie müssen Portable.BouncyCastle verwenden.

Sie können Ihre kodierten Ergebnisse testen, indem Sie das Ausgabe-JWT-Token mit Jwt.IO analysieren.

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;
    }
}
1
Chris