web-dev-qa-db-fra.com

Firebase 3: création d'un jeton d'authentification personnalisé à l'aide de .net et c #

J'essaie d'implémenter le mécanisme d'authentification Firebase 3 à l'aide de jetons personnalisés (comme décrit à l'adresse https: // firebase.google.com/docs/auth/server/create-custom-tokens).

Mon serveur est ASP.NET MVC Application.

Ainsi, conformément aux instructions ( https://firebase.google.com/docs/server/setup ), j'ai créé un compte de service pour mon application Firebase et généré une clé au format '.p12'.

Après cela, selon les instructions ici ( https://firebase.google.com/docs/auth/server/create-custom-tokens#create_custom_tokens_using_a_third-party_jwt_library ) J'ai essayé de générer un jeton personnalisé et de le signer à l'aide de la clé reçu à l'étape précédente. Pour la génération de jetons, j'ai utilisé la bibliothèque SystemIdentityModel.Tokens.Jwt de Microsoft. Le code a donc l'aspect suivant:

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

Puis essayé de vous connecter à l’application React Native en utilisant le SDK Javascript de Firebase, avec le code suivant:

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

Mais eu une erreur de Firebase en disant:

Erreur d'authentification de l'utilisateur Firebase. Code: auth/invalid-custom-token Message: Le format du jeton personnalisé est incorrect. Veuillez vérifier la documentation.

Essayer d'ajouter différentes revendications pour le contrôle de l'expiration des jetons n'a pas non plus aidé.

Aussi, j'ai essayé de générer des jetons avec la bibliothèque "dvsekhvalnov/jose-jwt" mais je ne peux pas le faire fonctionner avec l'algorithme "RS256".

Alors la question:

Toute suggestion sur ce que je fais mal?

13
Ilya Zatolokin

Cette solution .NET pure fonctionne pour moi, en utilisant Org.BouncyCastle ( https://www.nuget.org/packages/BouncyCastle/ ) et Jose.JWT ( https: // www. nuget.org/packages/jose-jwt/ ) bibliothèques.

J'ai suivi ces étapes:

  • Dans la console Firebase, cliquez sur l'icône "Cog" en haut à gauche, à côté du nom du projet, puis cliquez sur "Autorisations".
  • Sur la page IAM et Admin, cliquez sur "Comptes de service" sur la gauche.
  • Cliquez sur "Créer un compte de service" en haut, entrez un "Nom du compte de service", sélectionnez "Projet-> Editeur" dans la sélection de rôle, cochez la case "Fournir une nouvelle clé privée" et sélectionnez JSON.
  • Cliquez sur "Créer" et téléchargez le fichier JSON du compte de service et protégez-le.
  • Ouvrez le fichier JSON du compte de service dans un éditeur de texte approprié et placez les valeurs dans le code suivant:

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

Pour que cela fonctionne dans IIS, je devais modifier l'identité du pool de l'application et définir le paramètre "Charger le profil utilisateur" sur "true". 

14
Elliveny

N'ayant pas trouvé de réponse directe à la question jusqu'à présent, nous avons abouti à la solution suivante:

À l'aide des instructions ici a généré un fichier JSON avec les détails du compte de service et créé un serveur Node.js de base à l'aide du SDK du serveur Firebase, qui génère des jetons personnalisés corrects pour Firebase avec le code suivant:

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

Peut-être que quelqu'un trouvera cela utile.

2
Ilya Zatolokin

Le code @ Elliveny a fonctionné pour moi localement mais dans Azure une erreur est générée: "Le système ne peut pas trouver le fichier spécifié". Parce que j'ai un peu changé le code et que je travaille maintenant sur les deux serveurs.

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

La réponse de @ Elliveny a très bien fonctionné pour moi. Je l'utilise dans une application .NET Core 2.0 et je me suis inspiré de la réponse acceptée pour transformer cette solution en une classe pouvant être enregistrée en tant que dépendance singleton dans le conteneur de services d'application, ainsi qu'une configuration transmise via un constructeur afin que nous pouvons exploiter les secrets .NET pour la configuration de développement local et les variables d'environnement pour la configuration de production.

J'ai également mis de l'ordre dans la gestion du flux.

Remarque pour les développeurs .NET Core - vous devrez utiliser Portable.BouncyCastle

Vous pouvez tester vos résultats codés en analysant le jeton JWT de sortie avec 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;
    }
}
1
Chris