Firebase 3: создание пользовательского токена аутентификации с использованием .net и С#

Я пытаюсь реализовать механизм аутентификации Firebase 3 с использованием пользовательских токенов (как описано в https://firebase.google.com/docs/auth/server/create-custom-tokens).

Мой сервер - это приложение ASP.NET MVC.

Итак, согласно инструкциям (https://firebase.google.com/docs/server/setup) Я создал учетную запись службы для своего приложения Firebase и сгенерировал ключ в '. p12 '.

После этого в соответствии с инструкциями здесь (https://firebase.google.com/docs/auth/server/create-custom-tokens#create_custom_tokens_using_a_third-party_jwt_library) Я попытался создать пользовательский токен и подписать его с помощью ключа, полученного в предыдущем шаг. Для генерации маркера я использовал библиотеку SystemIdentityModel.Tokens.Jwt от Microsoft, поэтому код выглядит следующим образом:

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

Затем попытался войти в пользовательское приложение React Native с помощью Firebase Javascript SDK со следующим кодом:

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

Но у Firebase появилась ошибка:

Ошибка аутентификации пользователя Firebase. Код: auth/invalid-custom-token Сообщение: неправильный формат пользовательского токена. Пожалуйста, проверьте документацию.

Экспериментирование с добавлением различных требований к управлению истечением токена также не помогло.

Также я попытался создать токены с библиотекой "dvsekhvalnov/jose-jwt", но не смог заставить ее работать с алгоритмом "RS256".

Итак, вопрос:

Любое предложение о том, что я делаю неправильно?

Ответ 1

Это чистое .NET-решение работает для меня, используя Org.BouncyCastle(https://www.nuget.org/packages/BouncyCastle/) и Jose.JWT(https://www.nuget.org/packages/jose-jwt/).

Я выполнил следующие шаги:

  • В консоли Firebase нажмите значок "cog", который находится вверху слева, рядом с именем проекта и нажмите "Разрешения".
  • На странице IAM и Admin нажмите "Учетные записи службы" слева.
  • Нажмите "Создать учетную запись службы" вверху, введите "Имя учетной записи службы", выберите "Проект- > Редактор" в разделе "Выбор роли", установите флажок "Обрезать новый закрытый ключ" и выберите "JSON"
  • Нажмите "Создать" и загрузите файл JSON учетной записи службы и сохраните его.
  • Откройте файл JSON учетной записи службы в подходящем текстовом редакторе и поместите значения в следующий код:

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

Чтобы получить эту работу в IIS, мне нужно было изменить идентификатор пула приложений и установить для параметра "load user profile" значение true.

Ответ 2

Не нашли прямого ответа на этот вопрос до сих пор, поэтому на данный момент закончилось решение:

Используя здесь инструкцию, сгенерирован JSON файл с данными учетной записи службы и создан базовый сервер Node.js с использованием SDK сервера Firebase, который генерирует правильные пользовательские токены для Firebase со следующим кодом:

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

Возможно, кто-то найдет это полезным.

Ответ 3

@Ответ Elliveny отлично поработал у меня. Я использую его в приложении .NET Core 2.0 и основывался на принятом ответе, чтобы превратить это решение в класс, который может быть зарегистрирован как однозависимая зависимость в контейнере сервисов приложений, а также иметь конфигурацию, переданную через конструктор, чтобы мы можем использовать секреты .NET для локальной конфигурации конфигурации и переменных среды для конфигурации производства.

Я также немного обработал поток, обрабатывающий бит.

Примечание для разработчиков .NET Core - вам нужно будет использовать Portable.BouncyCastle

Вы можете протестировать свои закодированные результаты, проанализировав выходной токен JWT с помощью 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;
    }
}

Ответ 4

Код @Elliveny работал у меня локально, но в azure выдает ошибку: "Система не может найти указанный файл". Из-за того, что я немного изменил код и теперь работает на обоих серверах.

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;

    }