Skip to content

Instantly share code, notes, and snippets.

@HeikkiDev
Created October 14, 2021 12:20
Show Gist options
  • Select an option

  • Save HeikkiDev/8ce8b4cc6fa9c09c3f9cd9692a7b9010 to your computer and use it in GitHub Desktop.

Select an option

Save HeikkiDev/8ce8b4cc6fa9c09c3f9cd9692a7b9010 to your computer and use it in GitHub Desktop.
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.IO;
using System.Security.Claims;
using System.Security.Cryptography;
using Microsoft.IdentityModel.Tokens;
namespace FirebaseTest.Mobile.Helpers
{
public static class GoogleOAuthUtility
{
public static string CreateJwtForFirebaseMessaging(int expirationInMinutes = 30)
{
string privateKey = "-----BEGIN PRIVATE KEY----- {private_key field from your service account JSON file} -----END PRIVATE KEY-----\n";
long unixSeconds = DateTimeOffset.Now.ToUnixTimeSeconds();
long expirationUnixSeconds = DateTimeOffset.Now.AddMinutes(expirationInMinutes).ToUnixTimeSeconds();
RSAParameters rsaParameters = DecodeRsaParameters(privateKey);
var tokenHandler = new JwtSecurityTokenHandler();
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim("iss", "{client_email field from your service account JSON file}"),
new Claim("scope", "https://www.googleapis.com/auth/firebase.messaging"),
new Claim("aud", "https://oauth2.googleapis.com/token"),
new Claim("exp", expirationUnixSeconds.ToString()),
new Claim("iat", unixSeconds.ToString()),
}),
Expires = DateTimeOffset.Now.AddMinutes(expirationInMinutes).DateTime,
SigningCredentials = new SigningCredentials(new RsaSecurityKey(rsaParameters), SecurityAlgorithms.RsaSha256) // RS256 with Sha hash required by Google OAuth
};
SecurityToken token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
// Code of DecodeRsaParameters and below helpers methods and classes copied from https://github.com/googleapis/google-api-dotnet-client
private static RSAParameters DecodeRsaParameters(string pkcs8PrivateKey)
{
if (string.IsNullOrWhiteSpace(pkcs8PrivateKey))
{
throw new ArgumentException("Empty PrivateKey");
}
const string PrivateKeyPrefix = "-----BEGIN PRIVATE KEY-----";
const string PrivateKeySuffix = "-----END PRIVATE KEY-----";
pkcs8PrivateKey = pkcs8PrivateKey.Trim();
if (!pkcs8PrivateKey.StartsWith(PrivateKeyPrefix) || !pkcs8PrivateKey.EndsWith(PrivateKeySuffix))
{
throw new ArgumentException($"PKCS8 data must be contained within '{PrivateKeyPrefix}' and '{PrivateKeySuffix}'.", nameof(pkcs8PrivateKey));
}
string base64PrivateKey =
pkcs8PrivateKey.Substring(PrivateKeyPrefix.Length, pkcs8PrivateKey.Length - PrivateKeyPrefix.Length - PrivateKeySuffix.Length);
// FromBase64String() ignores whitespace, so further Trim()ing isn't required.
byte[] pkcs8Bytes = Convert.FromBase64String(base64PrivateKey);
object ans1 = Asn1.Decode(pkcs8Bytes);
object[] parameters = (object[])((object[])ans1)[2];
var rsaParmeters = new RSAParameters
{
Modulus = TrimLeadingZeroes((byte[])parameters[1]),
Exponent = TrimLeadingZeroes((byte[])parameters[2], alignTo8Bytes: false),
D = TrimLeadingZeroes((byte[])parameters[3]),
P = TrimLeadingZeroes((byte[])parameters[4]),
Q = TrimLeadingZeroes((byte[])parameters[5]),
DP = TrimLeadingZeroes((byte[])parameters[6]),
DQ = TrimLeadingZeroes((byte[])parameters[7]),
InverseQ = TrimLeadingZeroes((byte[])parameters[8]),
};
return rsaParmeters;
}
private static byte[] TrimLeadingZeroes(byte[] bs, bool alignTo8Bytes = true)
{
int zeroCount = 0;
while (zeroCount < bs.Length && bs[zeroCount] == 0) zeroCount += 1;
int newLength = bs.Length - zeroCount;
if (alignTo8Bytes)
{
int remainder = newLength & 0x07;
if (remainder != 0)
{
newLength += 8 - remainder;
}
}
if (newLength == bs.Length)
{
return bs;
}
byte[] result = new byte[newLength];
if (newLength < bs.Length)
{
Buffer.BlockCopy(bs, bs.Length - newLength, result, 0, newLength);
}
else
{
Buffer.BlockCopy(bs, 0, result, newLength - bs.Length, bs.Length);
}
return result;
}
private class Asn1
{
private enum Tag
{
Integer = 2,
OctetString = 4,
Null = 5,
ObjectIdentifier = 6,
Sequence = 16,
}
private class Decoder
{
public Decoder(byte[] bytes)
{
_bytes = bytes;
_index = 0;
}
private byte[] _bytes;
private int _index;
public object Decode()
{
Tag tag = ReadTag();
switch (tag)
{
case Tag.Integer:
return ReadInteger();
case Tag.OctetString:
return ReadOctetString();
case Tag.Null:
return ReadNull();
case Tag.ObjectIdentifier:
return ReadOid();
case Tag.Sequence:
return ReadSequence();
default:
throw new NotSupportedException($"Tag '{tag}' not supported.");
}
}
private byte NextByte() => _bytes[_index++];
private byte[] ReadLengthPrefixedBytes()
{
int length = ReadLength();
return ReadBytes(length);
}
private byte[] ReadInteger() => ReadLengthPrefixedBytes();
private object ReadOctetString()
{
byte[] bytes = ReadLengthPrefixedBytes();
return new Decoder(bytes).Decode();
}
private object ReadNull()
{
int length = ReadLength();
if (length != 0)
{
throw new InvalidDataException("Invalid data, Null length must be 0.");
}
return null;
}
private int[] ReadOid()
{
byte[] oidBytes = ReadLengthPrefixedBytes();
List<int> result = new List<int>();
bool first = true;
int index = 0;
while (index < oidBytes.Length)
{
int subId = 0;
byte b;
do
{
b = oidBytes[index++];
if ((subId & 0xff000000) != 0)
{
throw new NotSupportedException("Oid subId > 2^31 not supported.");
}
subId = (subId << 7) | (b & 0x7f);
} while ((b & 0x80) != 0);
if (first)
{
first = false;
result.Add(subId / 40);
result.Add(subId % 40);
}
else
{
result.Add(subId);
}
}
return result.ToArray();
}
private object[] ReadSequence()
{
int length = ReadLength();
int endOffset = _index + length;
if (endOffset < 0 || endOffset > _bytes.Length)
{
throw new InvalidDataException("Invalid sequence, too long.");
}
List<object> sequence = new List<object>();
while (_index < endOffset)
{
sequence.Add(Decode());
}
return sequence.ToArray();
}
private byte[] ReadBytes(int length)
{
if (length <= 0)
{
throw new ArgumentOutOfRangeException(nameof(length), "length must be positive.");
}
if (_bytes.Length - length < 0)
{
throw new ArgumentException("Cannot read past end of buffer.");
}
byte[] result = new byte[length];
Array.Copy(_bytes, _index, result, 0, length);
_index += length;
return result;
}
private Tag ReadTag()
{
byte b = NextByte();
int tag = b & 0x1f;
if (tag == 0x1f)
{
// A tag value of 0x1f (31) indicates a tag value of >30 (spec section 8.1.2.4)
throw new NotSupportedException("Tags of value > 30 not supported.");
}
else
{
return (Tag)tag;
}
}
private int ReadLength()
{
byte b0 = NextByte();
if ((b0 & 0x80) == 0)
{
return b0;
}
else
{
if (b0 == 0xff)
{
throw new InvalidDataException("Invalid length byte: 0xff");
}
int byteCount = b0 & 0x7f;
if (byteCount == 0)
{
throw new NotSupportedException("Lengths in Indefinite Form not supported.");
}
int result = 0;
for (int i = 0; i < byteCount; i++)
{
if ((result & 0xff800000) != 0)
{
throw new NotSupportedException("Lengths > 2^31 not supported.");
}
result = (result << 8) | NextByte();
}
return result;
}
}
}
public static object Decode(byte[] bs) => new Decoder(bs).Decode();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment