Czy istnieje przykład JSON Web Token (JWT) w C #?

103

Czuję, że biorę tutaj szalone tabletki. Zwykle w Internecie jest zawsze milion bibliotek i próbek do dowolnego zadania. Próbuję zaimplementować uwierzytelnianie za pomocą „konta usługi” Google przy użyciu tokenów sieciowych JSON (JWT), jak opisano tutaj .

Jednak istnieją tylko biblioteki klienckie w PHP, Python i Java. Nawet szukając przykładów JWT poza uwierzytelnianiem Google, istnieją tylko świerszcze i szkice na temat koncepcji JWT. Czy to naprawdę takie nowe i prawdopodobnie zastrzeżony system Google?

Próbka Java, która jest najbliższa, jaką udało mi się zinterpretować, wygląda dość intensywnie i onieśmielająco. W C # musi być coś, od czego mógłbym przynajmniej zacząć. Jakakolwiek pomoc byłaby świetna!

Levitikon
źródło
2
Peter ma twoją odpowiedź. JWT jest stosunkowo nowym formatem tokenów, dlatego próbki są wciąż trochę trudne do zdobycia, ale rozwija się bardzo szybko, ponieważ tokenów JWT są bardzo potrzebnym zamiennikiem SWT. Firma Microsoft wspiera format tokena, na przykład interfejsy API połączenia na żywo używają tokenów JWT.
Andrew Lavers,
Czy ma to coś wspólnego z App Engine?
Nick Johnson,

Odpowiedzi:

76

Dziękuję wszystkim. Znalazłem podstawową implementację tokena internetowego Json i rozszerzyłem ją o smak Google. Nadal nie udało mi się tego całkowicie rozwiązać, ale jest tam 97%. Ten projekt stracił na sile, więc miejmy nadzieję, że pomoże to komuś innemu na dobry start:

Uwaga: Zmiany, które wprowadziłem w podstawowej implementacji (nie pamiętam, gdzie ją znalazłem) to:

  1. Zmieniono HS256 -> RS256
  2. Zamieniono kolejność JWT i alg w nagłówku. Nie jestem pewien, kto się pomylił, Google lub specyfikacja, ale Google bierze to tak, jak jest poniżej, zgodnie z ich dokumentami.
public enum JwtHashAlgorithm
{
    RS256,
    HS384,
    HS512
}

public class JsonWebToken
{
    private static Dictionary<JwtHashAlgorithm, Func<byte[], byte[], byte[]>> HashAlgorithms;

    static JsonWebToken()
    {
        HashAlgorithms = new Dictionary<JwtHashAlgorithm, Func<byte[], byte[], byte[]>>
            {
                { JwtHashAlgorithm.RS256, (key, value) => { using (var sha = new HMACSHA256(key)) { return sha.ComputeHash(value); } } },
                { JwtHashAlgorithm.HS384, (key, value) => { using (var sha = new HMACSHA384(key)) { return sha.ComputeHash(value); } } },
                { JwtHashAlgorithm.HS512, (key, value) => { using (var sha = new HMACSHA512(key)) { return sha.ComputeHash(value); } } }
            };
    }

    public static string Encode(object payload, string key, JwtHashAlgorithm algorithm)
    {
        return Encode(payload, Encoding.UTF8.GetBytes(key), algorithm);
    }

    public static string Encode(object payload, byte[] keyBytes, JwtHashAlgorithm algorithm)
    {
        var segments = new List<string>();
        var header = new { alg = algorithm.ToString(), typ = "JWT" };

        byte[] headerBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(header, Formatting.None));
        byte[] payloadBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(payload, Formatting.None));
        //byte[] payloadBytes = Encoding.UTF8.GetBytes(@"{"iss":"761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5@developer.gserviceaccount.com","scope":"https://www.googleapis.com/auth/prediction","aud":"https://accounts.google.com/o/oauth2/token","exp":1328554385,"iat":1328550785}");

        segments.Add(Base64UrlEncode(headerBytes));
        segments.Add(Base64UrlEncode(payloadBytes));

        var stringToSign = string.Join(".", segments.ToArray());

        var bytesToSign = Encoding.UTF8.GetBytes(stringToSign);

        byte[] signature = HashAlgorithms[algorithm](keyBytes, bytesToSign);
        segments.Add(Base64UrlEncode(signature));

        return string.Join(".", segments.ToArray());
    }

    public static string Decode(string token, string key)
    {
        return Decode(token, key, true);
    }

    public static string Decode(string token, string key, bool verify)
    {
        var parts = token.Split('.');
        var header = parts[0];
        var payload = parts[1];
        byte[] crypto = Base64UrlDecode(parts[2]);

        var headerJson = Encoding.UTF8.GetString(Base64UrlDecode(header));
        var headerData = JObject.Parse(headerJson);
        var payloadJson = Encoding.UTF8.GetString(Base64UrlDecode(payload));
        var payloadData = JObject.Parse(payloadJson);

        if (verify)
        {
            var bytesToSign = Encoding.UTF8.GetBytes(string.Concat(header, ".", payload));
            var keyBytes = Encoding.UTF8.GetBytes(key);
            var algorithm = (string)headerData["alg"];

            var signature = HashAlgorithms[GetHashAlgorithm(algorithm)](keyBytes, bytesToSign);
            var decodedCrypto = Convert.ToBase64String(crypto);
            var decodedSignature = Convert.ToBase64String(signature);

            if (decodedCrypto != decodedSignature)
            {
                throw new ApplicationException(string.Format("Invalid signature. Expected {0} got {1}", decodedCrypto, decodedSignature));
            }
        }

        return payloadData.ToString();
    }

    private static JwtHashAlgorithm GetHashAlgorithm(string algorithm)
    {
        switch (algorithm)
        {
            case "RS256": return JwtHashAlgorithm.RS256;
            case "HS384": return JwtHashAlgorithm.HS384;
            case "HS512": return JwtHashAlgorithm.HS512;
            default: throw new InvalidOperationException("Algorithm not supported.");
        }
    }

    // from JWT spec
    private static string Base64UrlEncode(byte[] input)
    {
        var output = Convert.ToBase64String(input);
        output = output.Split('=')[0]; // Remove any trailing '='s
        output = output.Replace('+', '-'); // 62nd char of encoding
        output = output.Replace('/', '_'); // 63rd char of encoding
        return output;
    }

    // from JWT spec
    private static byte[] Base64UrlDecode(string input)
    {
        var output = input;
        output = output.Replace('-', '+'); // 62nd char of encoding
        output = output.Replace('_', '/'); // 63rd char of encoding
        switch (output.Length % 4) // Pad with trailing '='s
        {
            case 0: break; // No pad chars in this case
            case 2: output += "=="; break; // Two pad chars
            case 3: output += "="; break; // One pad char
            default: throw new System.Exception("Illegal base64url string!");
        }
        var converted = Convert.FromBase64String(output); // Standard base64 decoder
        return converted;
    }
}

A potem moja klasa JWT specyficzna dla Google:

public class GoogleJsonWebToken
{
    public static string Encode(string email, string certificateFilePath)
    {
        var utc0 = new DateTime(1970,1,1,0,0,0,0, DateTimeKind.Utc);
        var issueTime = DateTime.Now;

        var iat = (int)issueTime.Subtract(utc0).TotalSeconds;
        var exp = (int)issueTime.AddMinutes(55).Subtract(utc0).TotalSeconds; // Expiration time is up to 1 hour, but lets play on safe side

        var payload = new
        {
            iss = email,
            scope = "https://www.googleapis.com/auth/gan.readonly",
            aud = "https://accounts.google.com/o/oauth2/token",
            exp = exp,
            iat = iat
        };

        var certificate = new X509Certificate2(certificateFilePath, "notasecret");

        var privateKey = certificate.Export(X509ContentType.Cert);

        return JsonWebToken.Encode(payload, privateKey, JwtHashAlgorithm.RS256);
    }
}
Levitikon
źródło
9
Oryginalna implementacja wydaje się być biblioteką JWT Johna Sheehansa: github.com/johnsheehan/jwt
Torbjørn
Wygląda na to, że John's nie obsługuje algorytmów szyfrowania RS (flaga alg), ale ta wersja tak.
Ryan
15
Ta wersja NIE obsługuje poprawnie algorytmu podpisywania RS256! Haszuje tylko dane wejściowe z bajtami klucza jako sekretem, zamiast poprawnie szyfrować skrót, jak powinno być zrobione w PKI. Po prostu przełącza etykietę HS256 na etykietę RS256 bez odpowiedniej implementacji.
Hans Z.
1
Powyższy kod jest częściowo przedmiotem opisanego przez nią ataku bezpieczeństwa: auth0.com/blog/2015/03/31/… Jest podatny na „Jeśli serwer oczekuje na token podpisany za pomocą RSA, ale w rzeczywistości otrzymuje token podpisany za pomocą HMAC , pomyśli, że klucz publiczny jest w rzeczywistości tajnym kluczem HMAC. ”
BennyBechDk
@Levitikon Wszelkie pomysły, jak mogę zdekodować prywatny_klucz, który Google dostarcza w pliku JSON? Dzięki
bibscy
46

Po tych wszystkich miesiącach od pierwotnego pytania warto teraz wskazać, że Microsoft opracował własne rozwiązanie. Zobacz http://blogs.msdn.com/b/vbertocci/archive/2012/11/20/introducing-the-developer-preview-of-the-json-web-token-handler-for-the-microsoft-net -framework-4-5.aspx, aby uzyskać szczegółowe informacje.

Jouni Heikniemi
źródło
7
pakiet NuGet w tym blogu jest amortyzowany. Uważam, że nowy to nuget.org/packages/System.IdentityModel.Tokens.Jwt/ ...
Stan
3
@Stan ten link jest świetny, ale jest ustawiony na konkretną (i teraz przestarzałą) wersję. Będzie to zawsze wskazywać na najnowszą wersję. nuget.org/packages/System.IdentityModel.Tokens.Jwt
Jeffrey Harmon
3
Niektóre fragmenty kodu demonstrujące użycie (kodowanie / dekodowanie, symetryczne / asymetryczne) byłyby bardzo przydatne.
Ohad Schneider,
6

To jest moja implementacja walidacji JWT (Google) w .NET. Opiera się na innych implementacjach na Gists Stack Overflow i GitHub.

using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Net.Http;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;

namespace QuapiNet.Service
{
    public class JwtTokenValidation
    {
        public async Task<Dictionary<string, X509Certificate2>> FetchGoogleCertificates()
        {
            using (var http = new HttpClient())
            {
                var response = await http.GetAsync("https://www.googleapis.com/oauth2/v1/certs");

                var dictionary = await response.Content.ReadAsAsync<Dictionary<string, string>>();
                return dictionary.ToDictionary(x => x.Key, x => new X509Certificate2(Encoding.UTF8.GetBytes(x.Value)));
            }
        }

        private string CLIENT_ID = "xxx.apps.googleusercontent.com";

        public async Task<ClaimsPrincipal> ValidateToken(string idToken)
        {
            var certificates = await this.FetchGoogleCertificates();

            TokenValidationParameters tvp = new TokenValidationParameters()
            {
                ValidateActor = false, // check the profile ID

                ValidateAudience = true, // check the client ID
                ValidAudience = CLIENT_ID,

                ValidateIssuer = true, // check token came from Google
                ValidIssuers = new List<string> { "accounts.google.com", "https://accounts.google.com" },

                ValidateIssuerSigningKey = true,
                RequireSignedTokens = true,
                IssuerSigningKeys = certificates.Values.Select(x => new X509SecurityKey(x)),
                IssuerSigningKeyResolver = (token, securityToken, kid, validationParameters) =>
                {
                    return certificates
                    .Where(x => x.Key.ToUpper() == kid.ToUpper())
                    .Select(x => new X509SecurityKey(x.Value));
                },
                ValidateLifetime = true,
                RequireExpirationTime = true,
                ClockSkew = TimeSpan.FromHours(13)
            };

            JwtSecurityTokenHandler jsth = new JwtSecurityTokenHandler();
            SecurityToken validatedToken;
            ClaimsPrincipal cp = jsth.ValidateToken(idToken, tvp, out validatedToken);

            return cp;
        }
    }
}

Zwróć uwagę, że aby z niego skorzystać, musisz dodać odwołanie do pakietu NuGet System.Net.Http.Formatting.Extension. Bez tego kompilator nie rozpozna ReadAsAsync<>metody.

Tomasz
źródło
Dlaczego miałbyś to ustawić, IssuerSigningKeysjeśli IssuerSigningKeyResolverjest podany?
AsifM
@AsifMD Tak naprawdę nie wiem i nie mogę tego teraz przetestować. Może to działa bez ustawiania IssuerSigningKey. Musisz także zmienić kod resolwera, aby zażądać certyfikatów, ponieważ w przeciwnym razie pojawi się błąd w ciągu kilku dni, gdy Google zmieni swoje certyfikaty.
Thomas
+1 za najprostsze podejście. Użyto PM> Install-Package System.IdentityModel.Tokens.Jwt -Version 5.2.4 do obsługi System.IdentityModel
Karthick Jayaraman
1

Lepiej byłoby użyć standardowych i znanych bibliotek zamiast pisać kod od zera.

  1. JWT do kodowania i dekodowania tokenów JWT
  2. Bouncy Castle obsługuje szyfrowanie i deszyfrowanie, zwłaszcza RS256, aby uzyskać je tutaj

Korzystając z tych bibliotek, możesz wygenerować token JWT i podpisać go za pomocą RS256, jak poniżej.

    public string GenerateJWTToken(string rsaPrivateKey)
    {
        var rsaParams = GetRsaParameters(rsaPrivateKey);
        var encoder = GetRS256JWTEncoder(rsaParams);

        // create the payload according to the Google's doc
        var payload = new Dictionary<string, object>
        {
            { "iss", ""},
            { "sub", "" },
            // and other key-values according to the doc
        };

        // add headers. 'alg' and 'typ' key-values are added automatically.
        var header = new Dictionary<string, object>
        {
            { "kid", "{your_private_key_id}" },
        };

        var token = encoder.Encode(header,payload, new byte[0]);

        return token;
    }

    private static IJwtEncoder GetRS256JWTEncoder(RSAParameters rsaParams)
    {
        var csp = new RSACryptoServiceProvider();
        csp.ImportParameters(rsaParams);

        var algorithm = new RS256Algorithm(csp, csp);
        var serializer = new JsonNetSerializer();
        var urlEncoder = new JwtBase64UrlEncoder();
        var encoder = new JwtEncoder(algorithm, serializer, urlEncoder);

        return encoder;
    }

    private static RSAParameters GetRsaParameters(string rsaPrivateKey)
    {
        var byteArray = Encoding.ASCII.GetBytes(rsaPrivateKey);
        using (var ms = new MemoryStream(byteArray))
        {
            using (var sr = new StreamReader(ms))
            {
                // use Bouncy Castle to convert the private key to RSA parameters
                var pemReader = new PemReader(sr);
                var keyPair = pemReader.ReadObject() as AsymmetricCipherKeyPair;
                return DotNetUtilities.ToRSAParameters(keyPair.Private as RsaPrivateCrtKeyParameters);
            }
        }
    }

ps: klucz prywatny RSA powinien mieć następujący format:

----- BEGIN RSA PRIVATE KEY ----- {base64 formatted value} ----- END RSA PRIVATE KEY -----

Ehsan Mirsaeedi
źródło
0

Oto kolejny działający przykład tylko do REST dla kont usług Google uzyskujących dostęp do użytkowników i grup G Suite , uwierzytelniających się przez JWT . Było to możliwe tylko dzięki odbiciu bibliotek Google, ponieważ dokumentacja Google dotycząca tych interfejsów API jest nie do zniesienia . Każdy, kto jest przyzwyczajony do programowania w technologiach MS, będzie miał trudności ze zrozumieniem, jak wszystko idzie razem w usługach Google.

$iss = "<name>@<serviceaccount>.iam.gserviceaccount.com"; # The email address of the service account.
$sub = "[email protected]"; # The user to impersonate (required).
$scope = "https://www.googleapis.com/auth/admin.directory.user.readonly https://www.googleapis.com/auth/admin.directory.group.readonly";
$certPath = "D:\temp\mycertificate.p12";
$grantType = "urn:ietf:params:oauth:grant-type:jwt-bearer";

# Auxiliary functions
function UrlSafeEncode([String] $Data) {
    return $Data.Replace("=", [String]::Empty).Replace("+", "-").Replace("/", "_");
}

function UrlSafeBase64Encode ([String] $Data) {
    return (UrlSafeEncode -Data ([Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Data))));
}

function KeyFromCertificate([System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate) {
    $privateKeyBlob = $Certificate.PrivateKey.ExportCspBlob($true);
    $key = New-Object System.Security.Cryptography.RSACryptoServiceProvider;
    $key.ImportCspBlob($privateKeyBlob);
    return $key;
}

function CreateSignature ([Byte[]] $Data, [System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate) {
    $sha256 = [System.Security.Cryptography.SHA256]::Create();
    $key = (KeyFromCertificate $Certificate);
    $assertionHash = $sha256.ComputeHash($Data);
    $sig = [Convert]::ToBase64String($key.SignHash($assertionHash, "2.16.840.1.101.3.4.2.1"));
    $sha256.Dispose();
    return $sig;
}

function CreateAssertionFromPayload ([String] $Payload, [System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate) {
    $header = @"
{"alg":"RS256","typ":"JWT"}
"@;
    $assertion = New-Object System.Text.StringBuilder;

    $assertion.Append((UrlSafeBase64Encode $header)).Append(".").Append((UrlSafeBase64Encode $Payload)) | Out-Null;
    $signature = (CreateSignature -Data ([System.Text.Encoding]::ASCII.GetBytes($assertion.ToString())) -Certificate $Certificate);
    $assertion.Append(".").Append((UrlSafeEncode $signature)) | Out-Null;
    return $assertion.ToString();
}

$baseDateTime = New-Object DateTime(1970, 1, 1, 0, 0, 0, [DateTimeKind]::Utc);
$timeInSeconds = [Math]::Truncate([DateTime]::UtcNow.Subtract($baseDateTime).TotalSeconds);

$jwtClaimSet = @"
{"scope":"$scope","email_verified":false,"iss":"$iss","sub":"$sub","aud":"https://oauth2.googleapis.com/token","exp":$($timeInSeconds + 3600),"iat":$timeInSeconds}
"@;


$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($certPath, "notasecret", [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable);
$jwt = CreateAssertionFromPayload -Payload $jwtClaimSet -Certificate $cert;


# Retrieve the authorization token.
$authRes = Invoke-WebRequest -Uri "https://oauth2.googleapis.com/token" -Method Post -ContentType "application/x-www-form-urlencoded" -UseBasicParsing -Body @"
assertion=$jwt&grant_type=$([Uri]::EscapeDataString($grantType))
"@;
$authInfo = ConvertFrom-Json -InputObject $authRes.Content;

$resUsers = Invoke-WebRequest -Uri "https://www.googleapis.com/admin/directory/v1/users?domain=<required_domain_name_dont_trust_google_documentation_on_this>" -Method Get -Headers @{
    "Authorization" = "$($authInfo.token_type) $($authInfo.access_token)"
}

$users = ConvertFrom-Json -InputObject $resUsers.Content;

$users.users | ft primaryEmail, isAdmin, suspended;
Winicjusz
źródło
0

Oto lista klas i funkcji:

open System
open System.Collections.Generic
open System.Linq
open System.Threading.Tasks
open Microsoft.AspNetCore.Mvc
open Microsoft.Extensions.Logging
open Microsoft.AspNetCore.Authorization
open Microsoft.AspNetCore.Authentication
open Microsoft.AspNetCore.Authentication.JwtBearer
open Microsoft.IdentityModel.Tokens
open System.IdentityModel.Tokens
open System.IdentityModel.Tokens.Jwt
open Microsoft.IdentityModel.JsonWebTokens
open System.Text
open Newtonsoft.Json
open System.Security.Claims
    let theKey = "VerySecretKeyVerySecretKeyVerySecretKey"
    let securityKey = SymmetricSecurityKey(Encoding.UTF8.GetBytes(theKey))
    let credentials = SigningCredentials(securityKey, SecurityAlgorithms.RsaSsaPssSha256)
    let expires = DateTime.UtcNow.AddMinutes(123.0) |> Nullable
    let token = JwtSecurityToken(
                    "lahoda-pro-issuer", 
                    "lahoda-pro-audience",
                    claims = null,
                    expires =  expires,
                    signingCredentials = credentials
        )

    let tokenString = JwtSecurityTokenHandler().WriteToken(token)
Dzmitry Lahoda
źródło