Чи є приклад JSON Web Token (JWT) в C #?


101

Я відчуваю, що я тут приймаю божевільні таблетки. Зазвичай завжди існує мільйон бібліотеки та зразків, що плавають по Інтернету для будь-якого завдання. Я намагаюся реалізувати автентифікацію з "обліковим записом служби" Google за допомогою веб-токенів JSON (JWT), як описано тут .

Однак є лише бібліотеки клієнтів у PHP, Python та Java. Навіть шукаючи приклади JWT поза аутентифікацією Google, у концепції JWT є лише цвіркуни та протяги. Це справді така нова і, можливо, власна система Google?

Зразок java, який є найближчим мені вдалося інтерпретувати, виглядає досить інтенсивно і залякано. У C # повинно бути щось таке, з чого я міг би принаймні почати. Будь-яка допомога в цьому була б чудовою!


2
У Петра є ваша відповідь. JWT - це порівняно новий формат токенів, тому зразки ще мало важкі, але він дуже швидко зростає, оскільки JWT - це дуже потрібна заміна для SWT. Microsoft підтримує формат жетонів, API для прямого підключення, наприклад, використовує JWT.
Ендрю Лаверс

Чи має це щось спільне з App Engine?
Нік Джонсон

Відповіді:


74

Дякую всім. Я знайшов базову реалізацію веб-маркера Json і розширив його на смак Google. Я досі не розробив його повністю, але там 97%. Цей проект втратив свою пару, тому, сподіваємось, це допоможе комусь іншому отримати гарний старт:

Примітка. Зміни, внесені до базової реалізації (не пам'ятаю, де я її знайшов,):

  1. Змінено HS256 -> RS256
  2. Поміняв JWT і порядок alg у заголовку. Не впевнений, хто помилився, Google чи специфікація, але Google сприймає це так, як це внизу відповідно до їхніх документів.
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;
    }
}

А потім мій специфічний клас JWT для 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);
    }
}

9
Оригінальна реалізація , як видається, бібліотека Джон Sheehans JWT: github.com/johnsheehan/jwt
Torbjørn

Схоже, Джон не підтримує алгоритми шифрування RS (прапор alg), але ця версія є.
Райан

15
Ця версія НЕ підтримує алгоритм підписання RS256 правильно! Він лише хеширує вхід з ключовими байтами як секретом, а не правильно шифрує хеш, як це слід зробити в PKI. Він просто перемикає мітку HS256 на ярлик RS256 без належної реалізації.
Ганс З.

1
Вищевказаний код частково підпадає під атаку безпеки, описану в ній: auth0.com/blog/2015/03/31/… Він уразливий до „Якщо сервер очікує маркер, підписаний з RSA, але насправді отримує маркер, підписаний з HMAC , він подумає, що відкритий ключ є насправді секретним ключем HMAC ».
BennyBechDk

@Levitikon Будь-яка ідея, як я можу розшифрувати private_key, який Google постачає у файл JSON? Дякую
химерність

46

Після того, як минули всі ці місяці після оригінального питання, тепер варто зазначити, що Microsoft розробила власне рішення. Дивіться 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 для деталей.


7
пакет нута в цьому блозі знецінений. Я вважаю, що новий - nuget.org/packages/System.IdentityModel.Tokens.Jwt/…
Стен

3
@Stan це посилання є чудовим, але встановлено на конкретну (і тепер застаріла) версія. Це завжди вказуватиме на останню версію. nuget.org/packages/System.IdentityModel.Tokens.Jwt
Джеффрі Хармон

3
Деякі фрагменти коду, що демонструють використання (кодування / декодування, симетричні / асиметричні), були б дуже корисними.
Охад Шнайдер


12

Ось робочий приклад:

http://zavitax.wordpress.com/2012/12/17/logging-in-with-google-service-account-in-c-jwt/

Минуло досить багато часу, щоб зібрати шматки, розкидані по Інтернету, документи досить неповні ...


Нарешті рішення, яке було насправді підключення та гри. Дуже дякую! Це працювало для мене.
EmKay

6

Це моя реалізація (Google) JWT Validation в .NET. Він заснований на інших реалізаціях на Stack Overflow та GitHub gists.

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

Зауважте, що для його використання потрібно додати посилання на пакет NuGet System.Net.Http.Formatting.Extension. Без цього компілятор не розпізнає ReadAsAsync<>метод.


Навіщо вам потрібно встановити, IssuerSigningKeysякщо IssuerSigningKeyResolverнадано?
AsifM

@AsifMD Не знаю насправді і наразі не можу перевірити це. Можливо, це працює без встановлення IssuerSigningKey. Вам також потрібно змінити код розв’язувача, щоб запитувати сертифікати, оскільки в іншому випадку ви отримаєте помилку через кілька днів, коли Google змінює свої сертифікати.
Томас

+1 для цього найпростішого підходу. Використовується PM> Install-Package System.IdentityModel.Tokens.Jwt -Version 5.2.4 для підтримки System.IdentityModel
Karthick Jayaraman


1

Краще використовувати стандартні та відомі бібліотеки, а не писати код з нуля.

  1. JWT для кодування та декодування жетонів JWT
  2. Замок Bouncy підтримує шифрування та дешифрування, особливо RS256 отримати його тут

Використовуючи ці бібліотеки, ви можете створити маркер JWT та підписати його за допомогою RS256, як показано нижче.

    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: приватний ключ RSA повинен мати такий формат:

----- НАЧАЙТЕ ПРИВАТНИЙ КЛЮЧ RSA ----- {значення відформатованого базового64} ----- КРАЧНИЙ КЛЮЧ РАСА RSA -----


0

Ось ще один робочий приклад REST для облікових записів служб Google, які отримують доступ до користувачів і груп G Suite , що підтверджують автентифікацію через JWT . Це стало можливим лише через відображення бібліотек Google, оскільки документація Google про ці API не надто жахлива . Кожен, хто звик кодувати в MS-технологіях, буде важко зрозуміти, як все відбувається в сервісах Google.

$iss = "<name>@<serviceaccount>.iam.gserviceaccount.com"; # The email address of the service account.
$sub = "impersonate.user@mydomain.com"; # 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;

0

Ось перелік класів та функцій:

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)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.