มีตัวอย่าง JSON Web Token (JWT) ใน C # หรือไม่


102

ฉันรู้สึกเหมือนกำลังกินยาบ้าๆอยู่ที่นี่ โดยปกติแล้วจะมีไลบรารีและตัวอย่างกว่าล้านตัวอย่างลอยอยู่บนเว็บสำหรับงานใด ๆ ก็ตาม ฉันพยายามที่จะใช้การตรวจสอบด้วย Google "บัญชีบริการ" โดยการใช้ JSON เว็บโทเคน (JWT) ตามที่อธิบายไว้ที่นี่

อย่างไรก็ตามมีเฉพาะไลบรารีไคลเอ็นต์ใน PHP, Python และ Java แม้แต่การค้นหาตัวอย่าง JWT นอกการตรวจสอบสิทธิ์ของ Google ก็มีเพียงจิ้งหรีดและร่างแนวคิด JWT นี่เป็นระบบใหม่จริง ๆ และอาจเป็นระบบที่เป็นกรรมสิทธิ์ของ Google หรือไม่

ตัวอย่าง java ที่ใกล้เคียงที่สุดที่ฉันสามารถตีความได้ดูค่อนข้างเข้มข้นและน่ากลัว จะต้องมีบางอย่างใน C # ที่อย่างน้อยฉันก็เริ่มได้ ช่วยด้วยจะดีมาก!


2
ปีเตอร์มีคำตอบของคุณ JWT เป็นรูปแบบโทเค็นที่ค่อนข้างใหม่ซึ่งเป็นเหตุผลว่าทำไมตัวอย่างจึงยังหาได้ยาก แต่ก็เติบโตอย่างรวดเร็วเนื่องจาก JWT เป็นสิ่งที่จำเป็นอย่างมากในการทดแทน SWT Microsoft กำลังสำรองรูปแบบโทเค็นตัวอย่างเช่น live connect APIs ใช้ JWTs
Andrew Lavers

สิ่งนี้เกี่ยวข้องกับ App Engine หรือไม่
Nick Johnson

อาจซ้ำกันในการตรวจสอบความถูกต้อง
Thomas

คำตอบ:


75

ขอบคุณทุกคน ฉันพบการใช้งานพื้นฐานของ Json Web Token และขยายด้วยรสชาติของ 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
การใช้งานดั้งเดิมดูเหมือนจะเป็นห้องสมุด John Sheehans JWT: github.com/johnsheehan/jwt
Torbjørn

ดูเหมือนว่า John ไม่รองรับอัลกอริทึมการเข้ารหัส RS (alg flag) แต่เวอร์ชันนี้รองรับ
Ryan

15
เวอร์ชันนี้ไม่รองรับอัลกอริทึมการลงนาม RS256 อย่างถูกต้อง! เพียงแฮชอินพุตที่มีคีย์ไบต์เป็นความลับแทนที่จะเข้ารหัสแฮชอย่างถูกต้องตามที่ควรทำใน PKI เพียงแค่เปลี่ยนป้าย HS256 สำหรับป้าย RS256 โดยไม่ต้องใช้งานที่เหมาะสม
Hans Z

1
รหัสข้างต้นเป็นส่วนหนึ่งของการโจมตีด้านความปลอดภัยที่อธิบายไว้: auth0.com/blog/2015/03/31/…มีความเสี่ยงที่จะ“ หากเซิร์ฟเวอร์คาดว่าจะมีโทเค็นที่ลงนามด้วย RSA แต่ได้รับโทเค็นที่ลงชื่อด้วย HMAC จริง มันจะคิดว่าคีย์สาธารณะเป็นคีย์ลับของ HMAC”
BennyBechDk

@Levitikon ต้องการอะไรฉันจะถอดรหัส private_key ที่ google ให้มาในไฟล์ JSON ได้อย่างไร ขอบคุณ
bibscy

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 ในบล็อกนั้นเสื่อมราคา ฉันเชื่อว่าอันใหม่คือnuget.org/packages/System.IdentityModel.Tokens.Jwt/…
Stan

3
@ สแตนลิงค์นั้นยอดเยี่ยม แต่ถูกตั้งค่าเป็นเวอร์ชันเฉพาะ (และตอนนี้ล้าสมัยแล้ว) สิ่งนี้จะชี้ไปที่เวอร์ชันล่าสุดเสมอ nuget.org/packages/System.IdentityModel.Tokens.Jwt
Jeffrey Harmon

3
ข้อมูลโค้ดบางส่วนที่แสดงการใช้งาน (การเข้ารหัส / ถอดรหัสสมมาตร / ไม่สมมาตร) จะมีประโยชน์มาก
Ohad Schneider


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

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

System.Net.Http.Formatting.Extensionโปรดทราบว่าในการสั่งซื้อที่จะใช้มันคุณจะต้องเพิ่มการอ้างอิงถึงแพคเกจ NuGet หากไม่มีสิ่งนี้คอมไพลเลอร์จะไม่รู้จักReadAsAsync<>เมธอด


ทำไมคุณต้องตั้งค่าIssuerSigningKeysถ้าIssuerSigningKeyResolverมีให้?
AsifM

@AsifMD ไม่ทราบจริงๆและไม่สามารถทดสอบได้ในขณะนี้ อาจใช้งานได้โดยไม่ต้องตั้งค่า IssuerSigningKey คุณต้องเปลี่ยนรหัสตัวแก้ไขเพื่อขอใบรับรองเพราะมิฉะนั้นคุณจะได้รับข้อผิดพลาดในสองสามวันเมื่อ Google เปลี่ยนใบรับรอง
Thomas

+1 สำหรับแนวทางที่ง่ายที่สุดนี้ ใช้ PM> Install-Package System.IdentityModel.Tokens.Jwt -Version 5.2.4 เพื่อรองรับ System.IdentityModel
Karthick Jayaraman


1

จะดีกว่าถ้าใช้ไลบรารีมาตรฐานและมีชื่อเสียงแทนการเขียนโค้ดตั้งแต่เริ่มต้น

  1. JWTสำหรับการเข้ารหัสและถอดรหัสโทเค็น JWT
  2. Bouncy Castleรองรับการเข้ารหัสและถอดรหัสโดยเฉพาะ 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 ควรมีรูปแบบต่อไปนี้:

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


0

นี่เป็นอีกส่วนที่เหลือเพียงตัวอย่างการทำงานสำหรับการให้บริการบัญชี Google เข้าถึงG สวีทผู้ใช้และกลุ่มตรวจสอบผ่าน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)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.