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