Using ES256 algorithm with jwt-dotnet for Apple AppStore - c#

I'm trying to generate a jwt token to connect to AppStore API. I'm using the jwt-dotnet library to do this.
Apple requires ES256 to be used and the jwt-dotnet is asking for a public key to do the job. I only downloaded a private key from AppStore. How do I handle this?
Here's my code:
public static string GenerateAppStoreJwtToken()
{
var header = new Dictionary<string, object>()
{
{ "kid", "MY_VALUE" },
{ "typ", "JWT" }
};
var scope = new string[1] { "GET /v1/apps?filter[platform]=IOS" };
var payload = new Dictionary<string, object>
{
{ "iss", "MY_VALUE" },
{ "iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds() },
{ "exp", DateTimeOffset.UtcNow.AddMinutes(20).ToUnixTimeSeconds() },
{ "aud", "appstoreconnect-v1" },
{ "scope", scope }
};
IJwtAlgorithm algorithm = new ES256Algorithm(???); // What am I going to use here?
IJsonSerializer serializer = new JsonNetSerializer();
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
var token = encoder.Encode(header, payload, privateKey);
return token;
}

For anyone, like me, who usew JWT-dotnet elsewhere so doesn't want to use a different JWT package, this worked:
Converted the apple private key by removing the header and footer ("-----BEGIN PRIVATE KEY-----" etc) and removing the end of line characters to make a single string for easier storage.
Convert from Base64 and store in a ReadOnlySpan
ReadOnlySpan<byte> keyAsSpan = Convert.FromBase64String(key);
var prvKey = ECDsa.Create();
prvKey.ImportPkcs8PrivateKey(keyAsSpan,out var read);
Create the algorithm. A blank ECDsa instance is needed to prevent an NullException but it is not needed just for signing the token, only verifying which isn't necessary.
IJwtAlgorithm algorithm = new ES256Algorithm(ECDsa.Create(), prvKey)
I was able to receive a reply token from apple using this method.

Here's the final solution that worked for me. I ended up switching to jose-jwt but I'm pretty sure you can handle the same thing with jwt-dotnet. I just found working with jose-jwt a bit easier. Here's the link to jose-jwt: https://github.com/dvsekhvalnov/jose-jwt
And here's the final code. Please note that I did indeed use the private key I find in the p8 file and didn't have to convert anything. So the privateKey parameter I'm passing to the GenerateAppStoreJwtToken() function comes directly from the p8 file.
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using Jose;
public static string GenerateAppStoreJwtToken(string privateKey)
{
var header = new Dictionary<string, object>()
{
{ "alg", "ES256" },
{ "kid", "MY_VALUE" },
{ "typ", "JWT" }
};
var scope = new string[1] { "GET /v1/apps?filter[platform]=IOS" };
var payload = new Dictionary<string, object>
{
{ "iss", "MY_VALUE" },
{ "iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds() },
{ "exp", DateTimeOffset.UtcNow.AddMinutes(15).ToUnixTimeSeconds() },
{ "aud", "appstoreconnect-v1" },
{ "scope", scope }
};
CngKey key = CngKey.Import(Convert.FromBase64String(privateKey), CngKeyBlobFormat.Pkcs8PrivateBlob);
string token = JWT.Encode(payload, key, JwsAlgorithm.ES256, header);
return token;
}

Related

Encryption with public key in Jose-JWT

I think this question is not a dupe, so I will try to explain my situation.
I'm testing JWT, more specifically JOSE-JWT lib from Github, and well, I'm having troubles.
I'm generating a private-public key pair and sending to the client the public key, using PHP and phpseclib. Everything is correct as you can see there. My client is receiving the JSON and converting it to a object and extracting it to a string using JSON.NET.
I'm using BouncyCastle and an answer from Stackoverflow with a little modifications to read directly from a string instead from a File.
public static RSACryptoServiceProvider GetRSAProviderFromPemFile(string pemfile)
{
return GetRSAProviderFromPemString(File.ReadAllText(pemfile).Trim());
}
public static RSACryptoServiceProvider GetRSAProviderFromPemString(string pemstr)
{
bool isPrivateKeyFile = true;
if (pemstr.StartsWith(pempubheader) && pemstr.EndsWith(pempubfooter))
isPrivateKeyFile = false;
byte[] pemkey;
if (isPrivateKeyFile)
pemkey = DecodeOpenSSLPrivateKey(pemstr);
else
pemkey = DecodeOpenSSLPublicKey(pemstr);
if (pemkey == null)
return null;
if (isPrivateKeyFile)
return DecodeRSAPrivateKey(pemkey);
else
return DecodeX509PublicKey(pemkey);
}
And both of them are giving me problems, with the answer and using docs from Jose repo:
var payload1 = new Dictionary<string, object>()
{
{ "sub", "mr.x#contoso.com" },
{ "exp", 1300819380 }
};
Console.WriteLine("Jose says: {0}", JWT.Encode(payload1, pubkey, JwsAlgorithm.RS256));
Exception:
English equivalent: http://unlocalize.com/es/74799_Keyset-does-not-exist.html
And with Bouncy Castle:
var claims = new List<Claim>();
claims.Add(new Claim("claim1", "value1"));
claims.Add(new Claim("claim2", "value2"));
claims.Add(new Claim("claim3", "value3"));
Console.WriteLine("Bouncy Castle says: {0}", Helpers.CreateToken(claims, pubkeyStr));
Exception:
CreateToken method extracted from here: https://stackoverflow.com/a/44857593/3286975
I did a little modification to this method:
public static string CreateToken(List<Claim> claims, string privateRsaKey)
{
RSAParameters rsaParams;
using (var tr = new StringReader(privateRsaKey))
{
var pemReader = new PemReader(tr);
var keyPair = pemReader.ReadObject() as AsymmetricCipherKeyPair;
if (keyPair == null)
{
throw new Exception("Could not read RSA private key");
}
//var privateRsaParams = keyPair.Private as RsaPrivateCrtKeyParameters;
rsaParams = DotNetUtilities.ToRSAParameters(keyPair.Public as RsaKeyParameters); //DotNetUtilities.ToRSAParameters(privateRsaParams);
}
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
{
rsa.ImportParameters(rsaParams);
Dictionary<string, object> payload = claims.ToDictionary(k => k.Type, v => (object)v.Value);
return Jose.JWT.Encode(payload, rsa, Jose.JwsAlgorithm.RS256);
}
}
In both cases is like the encrypter methods are looking for the private key (in the client client????)... So, My question is why this examples are using private keys in the client-side, if the Wikipedia says this:
Source: https://en.wikipedia.org/wiki/Public-key_cryptography
And in several cases I found what I think is right:
https://connect2id.com/products/nimbus-jose-jwt/examples/jwt-with-rsa-encryption
In this Java example, this uses public key to encrypt data, not private one.
I don't know why C# examples are using private keys in the client-side, this is ilogical, can somebody explain me why, and how can I solve this problems?
I have found an answer for one of the two problems I have, and is that I haven't readed completely the JOSE-JWT repo, it says:
var payload = new Dictionary<string, object>()
{
{ "sub", "mr.x#contoso.com" },
{ "exp", 1300819380 }
};
var publicKey=... //Load it from there you need
string token = Jose.JWT.Encode(payload, publicKey, JweAlgorithm.RSA_OAEP, JweEncryption.A256GCM);
I have realized tha Bouncy Castle is only a API to manipulate public-private keys, the encryptation-decrypting work is done by JOSE-JWT. So, my question is solved.

Signed and Encrypt from KeyPair string

I'm a newbie in jwt and after read a lot of web pages I've not found examples of how to generate a token (signed and encrypted) with keypairs generated from this website https://mkjwk.org/. I think it can't be very difficult. I think this is the way to generate the signed token (I don't know if it's correct):
RSACryptoServiceProvider rsaCrypto = new RSACryptoServiceProvider();
var payload = new Dictionary<string, object>()
{
{"jti", "ORIGEN_" + Guid.NewGuid().ToString()},
{"iat", DateTime.UtcNow},
{"exp", DateTime.UtcNow.AddYears(1)},
{"login", "USER" },
{"password", "PASSWORD" },
{"origen", "ORIGEN" }
};
// Contains both public and private keys to sign
var headers = new Dictionary<string, object>()
{
{ "kty", "RSA" },
{ "d", "A7Q8cttv_CSG4CJkX_xlU5lUoeRrCPZpyZx9eVaD7zi-tE7wDPKNmJPRP6uR_LA2YVXMmfY9w8q1_v_MiYxkYnFgZqNZlKdwucSQUlnfX5Tt806qh_323h5NnHrKweL-98_d8R4RuZXCWEQ3X0QDCVfccaLVVqLJy8S5zlx0aAVuBJxLxBHFRO700qdUN-RaMjHULoOnE1KbwmfKPfGlLL0YWPHQ9t-qIBh6OSZsDZh30K4VLF8sRXkGgn81_Byp4hK9tCfG98R6fWUM2_FCQrC9R1hO-KTsLffRzMboWe-2ymZGQfZKO-gtFaQH7_AjdVnQYMyKhSSCGYAAroSZAQ"},
{ "e", "AQAB" },
{ "use", "enc"},
{ "kid", "RPA" },
{ "alg", "RS256" },
{ "n", "qJPwMcHtb7xFGGczn20IiEtrPVehquyT6lxIJa_e4vcZE33uM6myVZWocTZWzTDmrNT3bJghEpLOhrgYatT3QnJIiTM9KAD01kYPc5cP5yo6Wmu0YjivqL3Rj7dUvi2pvl7juwYxt1_8zfdnBN5GpBIYcaY3ulVo_OSL7TOxJrua5IMhilQz6kqta3-Rgz3GSglOs94RHRvorYxMyHPQ6KhwSlh_zLzJQZ-0-AZ4yaMPdVwEaaEJpL-odYmRudX4E0t42dExLf_q1rpRfvTcdFSwfsJ7FmQcOtlc340WUgr4BHJfwrNIE4i-TFqrB4zSQJVKHlBfLeGKiYZQPD7igw" }
};
string tokenSigned = JWT.Encode(payload, rsaCrypto, JwsAlgorithm.RS256, headers);
I get a token (I think is signed) but if I put this token in this web site https://jwt.io/, I get an error "Invalid Signature".
Also I would like to encrypt this token with the public key. So I do this:
// Another public key to encrypt
headers = new Dictionary<string, object>()
{
{ "kty", "RSA" },
{ "e", "AQAB" },
{ "use", "enc"},
{ "alg", "RS256" },
{ "n", "ldMvqNDlz8-ABqEhqjtT0qvjKKbJMQ4J6GEi-7QrY-EUtyjCE7cOriHrYmbjt3o3zXwUTyOp0-twnF5j1HXFwVk7_XNsZz7LUmGNtmnqgB2iw2xhS7LAicN0RRgIbxWRDLOaaZ-49QumX6_r_jLNtIspKiFiuUNf2s0ipeAjWBFquiiqTMBd98z3pS-vC5y0CfzPbTSLSinikrHkIW2uO4FNHWZpoo8npn7vwWtAJjknWhaFi2s9P5kzUk4Mpbdx4DxUJ9ZvUi9SmdvH2vUzwGe0lxyvlw0DAMMWAT9TmsiKzBeXTY6rQ1-2Edn4F9S5kkPNOh1NqJoebz50-Bpl6w" }
};
string tokenEncrypted = JWT.Encode(payload, tokenSigned , JweAlgorithm.RSA_OAEP, JweEncryption.A256GCM, extraHeaders: headers);
But I always get the error "RsaKeyManagement alg expects key to be of RSACryptoServiceProvider type.". I've already search about this error but I don't have anything clear.
Please, anyone can help me? I'm not sure if I'm on the right way.
I use jose-jwt for .net because I've read that the library System.IdentityModel.Tokens.Jwt can't encrypt.
Thank you.
Regards.
In this case I had a jwk and I wanted to sign it with my private key and encrypt it with a customer public key with c# jose-jwt library. This jwk is the same all the time, so I only need to generate it once. To do that, you need to create an RSA object and then use the Encode method of the library to sign and encrypt.
A jwk has severals parameters:
JSON Web Key (JWK): A JSON object that represents a cryptographic key.
The members of the object represent properties of the key, including
its value.
These are some of the parameters:
p = 'RSA secret prime';
kty = 'Key Type';
q = 'RSA secret prime';
d = 'RSA secret exponent';
e = 'RSA public exponent';
kid = 'Key ID';
n = 'RSA public modulus';
use = 'Public Key Use';
alg = 'Algorithm'
But in my case I only had some of them: d, e, n, kty, use, kid, alg. The problem is that if you have e and d parameters, you also need p and q, because you can't construct private key with .NET without primes (P and Q).
The solution was to part the problem in two parts:
JAVA part: create a complete jwk with the Nimbus JOSE+JWT JAVA library:
C# parts:
Use the previous jwk to create an RSA object in C# with c# jose-jwt library. Like this:
var js = new JavaScriptSerializer();
// json is the result returned by java
var jwk = js.Deserialize<IDictionary<string, string>>(json);
byte[] p = Base64Url.Decode(jwk["p"]);
byte[] q = Base64Url.Decode(jwk["q"]);
byte[] d = Base64Url.Decode(jwk["d"]);
byte[] e = Base64Url.Decode(jwk["e"]);
byte[] qi = Base64Url.Decode(jwk["qi"]);
byte[] dq = Base64Url.Decode(jwk["dq"]);
byte[] dp = Base64Url.Decode(jwk["dp"]);
byte[] n = Base64Url.Decode(jwk["n"]);
RSA key = RSA.Create();
RSAParameters keyParams = new RSAParameters();
keyParams.P = p;
keyParams.Q = q;
keyParams.D = d;
keyParams.Exponent = e;
keyParams.InverseQ = qi;
keyParams.DP = dp;
keyParams.DQ = dq;
keyParams.Modulus = n;
key.ImportParameters(keyParams);
Once you have an RSA object, you can sign it:
var payload = new Dictionary<string, object>()
{
{"user", USER },
{"password", PASSWORD }
};
string tokenSigned = JWT.Encode(payload, key, JwsAlgorithm.RS256);
You can find the original solution in the library author web page.
Regards.

JWT error IDX10634: Unable to create the SignatureProvider C#

I'm trying to run my app but it get stuck with the following error:
System.NotSupportedException HResult=0x80131515 Message=IDX10634:
Unable to create the SignatureProvider. Algorithm: '[PII is hidden by
default. Set the 'ShowPII' flag in IdentityModelEventSource.cs to true
to reveal it.]', SecurityKey: '[PII is hidden by default. Set the
'ShowPII' flag in IdentityModelEventSource.cs to true to reveal it.]'
is not supported.
Where
Algorithm is RS256
It stucks on executing this instruction: var sectoken = tokenHandler.CreateToken(tokenDescriptor);
What does it mean? What went wrong in my code? How can I solve this?
Here's my code:
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
//...
public class TokenManager
{
private string unencoded_key = "CaptainDeadpool";
private string encoded_key = "CaptainDeadpool";
//...
public TokenManager()
{
var plainTextBytes = Encoding.UTF8.GetBytes(unencoded_key);
encoded_key = Convert.ToBase64String(plainTextBytes);
}
public string CreateFromUsername(string usr, int? timer)
{
if (timer == null) { timer = 30; }
double timeadd = Convert.ToDouble(timer);
var secret = Convert.FromBase64String(encoded_key);
var tokenHandler = new JwtSecurityTokenHandler();
var actual = DateTime.UtcNow;
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, usr) }),
Expires = actual.AddMinutes(timeadd),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(secret), SecurityAlgorithms.RsaSha256Signature)
};
var sectoken = tokenHandler.CreateToken(tokenDescriptor);
var stringtoken = tokenHandler.WriteToken(sectoken);
return stringtoken;
}
//...
Here's my tokenDescriptor's content while issuing the error:
No idea what that error message means, but it doesn't matter I think, because your code is logically wrong. RSA is assymetric algorithm, but you are trying to use SymmetricSecurityKey with it.
So either use another (symmetric) signature algorithm (and ensure that your key size is valid for this algorithm), for example:
// adjust key size
private string unencoded_key = "CaptainDeadpool!";
private string encoded_key = "CaptainDeadpool!";
// ...
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(secret),
SecurityAlgorithms.HmacSha256Signature)
Or provide valid key, for example:
private readonly RSA _rsa;
public TokenManager() {
// import instead of creating new, if necessary
_rsa = new RSACryptoServiceProvider(2048);
}
// ...
SigningCredentials = new SigningCredentials(
new RsaSecurityKey(_rsa),
SecurityAlgorithms.RsaSha256Signature)
I had the same problem when using hmacSha256. if your security key is too short, you may receive that error. I increased the size of the secret secureKey and that resolved my problem.
var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("You_Need_To_Provide_A_Longer_Secret_Key_Here"));

Pushsharp support of Apple Push Notification Authentication Key

Does pushsharp supports a new Apple approach for sending APN using Apple Push Notification Authentication Key (which never expires) instead of using Certificates? Is any way to use it with pushsharp? If not, is there any other C# library to support it?
here you are:
private string GetToken()
{
var algorithm = "ES256";
var teamID = "teamID";
var apnsKeyID = "apnsKeyID";
var apnsAuthKeyPath = #"apnsAuthKeyPath";
var epochNow = DateTimeOffset.Now.ToUnixTimeSeconds();
var header = new Dictionary<string, object>()
{
{ "alg", algorithm },
{ "kid" , apnsKeyID }
};
var payload = new Dictionary<string, object>()
{
{ "iss", teamID },
{ "iat", epochNow }
};
var privateKey = GetPrivateKey(apnsAuthKeyPath);
var token = Jose.JWT.Encode(payload, privateKey, algorithm: Jose.JwsAlgorithm.ES256, extraHeaders: header);
return token;
}
private CngKey GetPrivateKey(string apnsAuthKey)
{
using (var reader = File.OpenText(apnsAuthKey))
{
var ecPrivateKeyParameters = (ECPrivateKeyParameters)new PemReader(reader).ReadObject();
var x = ecPrivateKeyParameters.Parameters.G.AffineXCoord.GetEncoded();
var y = ecPrivateKeyParameters.Parameters.G.AffineYCoord.GetEncoded();
var d = ecPrivateKeyParameters.D.ToByteArrayUnsigned();
return EccKey.New(x, y, d);
}
}
then you can simply call send method in apns.

Firebase 3: creating a custom authentication token using .net and c#

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?
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="serviceaccountname#projectname.iam.gserviceaccount.com";
public static string firebasePayloadSUB="serviceaccountname#projectname.iam.gserviceaccount.com";
// 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.
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.
#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;
}
}
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;
}

Categories