Có ví dụ nào về mã thông báo web JSON (JWT) trong C # không?


101

Tôi cảm thấy như tôi đang uống thuốc điên ở đây. Thông thường, luôn có hàng triệu thư viện và mẫu trôi nổi trên web cho bất kỳ tác vụ nào. Tôi đang cố gắng triển khai xác thực với "Tài khoản dịch vụ" của Google bằng cách sử dụng Mã thông báo web JSON (JWT) như được mô tả ở đây .

Tuy nhiên, chỉ có các thư viện khách bằng PHP, Python và Java. Ngay cả khi tìm kiếm các ví dụ JWT bên ngoài xác thực của Google, cũng chỉ có các bản nháp và bản nháp về khái niệm JWT. Điều này có thực sự quá mới và có thể là một hệ thống độc quyền của Google không?

Mẫu java gần nhất mà tôi có thể quản lý để diễn giải trông khá chuyên sâu và đáng sợ. Phải có một cái gì đó ngoài kia trong C # mà ít nhất tôi có thể bắt đầu. Bất kỳ trợ giúp với điều này sẽ là tuyệt vời!


2
Peter có câu trả lời của bạn. JWT là một định dạng mã thông báo tương đối mới, đó là lý do tại sao các mẫu vẫn còn hơi khó tìm, nhưng nó đang phát triển rất nhanh vì JWT là sự thay thế rất cần thiết cho SWT. Microsoft đang ủng hộ định dạng mã thông báo, ví dụ như các API kết nối trực tiếp sử dụng JWT.
Andrew Lavers

Điều này có liên quan gì đến App Engine không?
Nick Johnson

Có thể có bản sao của Xác thực Mã thông báo ID JWT
Thomas

Câu trả lời:


74

Cảm ơn mọi người. Tôi đã tìm thấy một triển khai cơ bản của Mã thông báo web Json và mở rộng trên đó với hương vị của Google. Tôi vẫn chưa làm cho nó hoàn toàn hoạt động nhưng nó đã đến 97%. Dự án này đã mất hơi nước, vì vậy hy vọng điều này sẽ giúp ai đó khác có được khởi đầu thuận lợi:

Lưu ý: Những thay đổi tôi đã thực hiện đối với triển khai cơ sở (Không thể nhớ tôi đã tìm thấy nó ở đâu,) là:

  1. Đã thay đổi HS256 -> RS256
  2. Đã hoán đổi JWT và thứ tự alg trong tiêu đề. Không chắc ai đã sai, Google hay thông số kỹ thuật, nhưng google sẽ xử lý nó theo cách Đó là bên dưới theo tài liệu của họ.
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;
    }
}

Và sau đó là lớp JWT cụ thể trên google của tôi:

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
Triển khai ban đầu dường như là thư viện JWT của John Sheehans: github.com/johnsheehan/jwt
Torbjørn

Có vẻ như John's không hỗ trợ thuật toán mã hóa RS (cờ alg) nhưng phiên bản này thì có.
Ryan

15
Phiên bản này KHÔNG hỗ trợ thuật toán ký RS256 một cách chính xác! Nó chỉ băm đầu vào với các byte khóa là bí mật thay vì mã hóa băm đúng cách như nên được thực hiện trong PKI. Nó chỉ chuyển đổi nhãn HS256 cho nhãn RS256 mà không thực hiện đúng cách.
Hans Z.

1
Đoạn mã trên là đối tượng của cuộc tấn công bảo mật được mô tả: auth0.com/blog/2015/03/31/… Nó dễ bị tấn công “Nếu máy chủ đang mong đợi một mã thông báo được ký bằng RSA, nhưng thực sự nhận được một mã thông báo được ký bằng HMAC , nó sẽ nghĩ rằng khóa công khai thực sự là một khóa bí mật HMAC. ”
BennyBechDk

@Levitikon Bất kỳ lý tưởng nào tôi có thể giải mã private_key mà google cung cấp trong tệp JSON? Cảm ơn
bibscy

46

Sau tất cả những tháng trôi qua sau câu hỏi ban đầu, bây giờ đáng để chỉ ra rằng Microsoft đã nghĩ ra một giải pháp của riêng họ. Xem http://blogs.msdn.com/b/vbertocci/archive/2012/11/20/introductioning-the-developer-preview-of-the-json-web-token-handler-for-the-microsoft-net -framework-4-5.aspx để biết chi tiết.


7
gói nuget trong blog đó bị giảm giá trị. Tôi tin rằng cái mới là nuget.org/packages/System.IdentityModel.Tokens.Jwt/…
Stan

3
@Stan liên kết đó rất tuyệt, nhưng được đặt thành một phiên bản cụ thể (và hiện đã lỗi thời). Điều này sẽ luôn trỏ đến phiên bản mới nhất. nuget.org/packages/System.IdentityModel.Tokens.Jwt
Jeffrey Harmon

3
Một số đoạn mã thể hiện cách sử dụng (mã hóa / giải mã, đối xứng / không đối xứng) sẽ rất hữu ích.
Ohad Schneider



6

Đây là cách tôi triển khai Xác thực JWT (Google) trong .NET. Nó dựa trên các triển khai khác trên Stack Overflow và 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;
        }
    }
}

Lưu ý rằng, để sử dụng nó, bạn cần thêm tham chiếu đến gói NuGet System.Net.Http.Formatting.Extension. Nếu không có điều này, trình biên dịch sẽ không nhận ra ReadAsAsync<>phương thức.


Tại sao bạn cần đặt IssuerSigningKeysnếu IssuerSigningKeyResolverđược cung cấp?
AsifM

@AsifMD Không biết thực sự và không thể kiểm tra nó vào lúc này. Có thể nó hoạt động mà không cần thiết lập IssuerSignKey. Bạn cũng cần thay đổi mã trình phân giải để yêu cầu chứng chỉ vì nếu không, bạn sẽ gặp lỗi sau vài ngày khi Google thay đổi chứng chỉ của nó.
Thomas

+1 cho cách tiếp cận đơn giản nhất này. Đã sử dụng PM> Install-Package System.IdentityModel.Tokens.Jwt -Version 5.2.4 để hỗ trợ System.IdentityModel
Karthick Jayaraman


1

Sẽ tốt hơn nếu sử dụng các thư viện tiêu chuẩn và nổi tiếng thay vì viết mã từ đầu.

  1. JWT để mã hóa và giải mã mã thông báo JWT
  2. Bouncy Castle hỗ trợ mã hóa và giải mã, đặc biệt RS256 lấy tại đây

Sử dụng các thư viện này, bạn có thể tạo mã thông báo JWT và ký mã bằng RS256 như bên dưới.

    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: khóa cá nhân RSA phải có định dạng sau:

----- BẮT ĐẦU KHÓA RIÊNG TƯ RSA ----- {giá trị được định dạng base64} ----- KẾT THÚC KHÓA RIÊNG RSA -----


0

Đây là một ví dụ hoạt động khác chỉ dành cho REST dành cho các Tài khoản dịch vụ của Google truy cập Người dùng và Nhóm G Suite , xác thực thông qua JWT . Điều này chỉ có thể thực hiện được thông qua phản ánh của các thư viện Google, vì tài liệu của Google về các API này quá khủng khiếp . Bất kỳ ai từng viết mã trong các công nghệ MS sẽ gặp khó khăn trong việc tìm ra cách mọi thứ kết hợp với nhau trong các dịch vụ của 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

Đây là danh sách các lớp và hàm:

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)
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.