Ver Fonte

Creating a JWT with a custom demo JWK

Lukas Angerer há 2 anos atrás
pai
commit
1789ae4b68
8 ficheiros alterados com 134 adições e 24 exclusões
  1. 66 11
      CredentialManager.cs
  2. 8 0
      JwtConfig.cs
  3. 1 0
      Passwordless.csproj
  4. 22 3
      Program.cs
  5. 1 1
      data/my username.json
  6. 14 0
      demo-jwk.json
  7. 2 2
      wwwroot/index.html
  8. 20 7
      wwwroot/main.js

+ 66 - 11
CredentialManager.cs

@@ -1,8 +1,14 @@
-using System.Text;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
 using System.Text.Json;
 using System.Text.Json.Nodes;
 using Fido2NetLib;
 using Fido2NetLib.Objects;
+using JWT.Algorithms;
+using JWT.Builder;
+using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Tokens;
 
 namespace Passwordless;
 
@@ -11,11 +17,15 @@ public class CredentialManager
     private readonly IFido2 _fido2;
     private readonly OptionsCache _optionsCache;
     private readonly JsonSerializerOptions _jsonOptions;
+    private readonly JwtConfig _jwtConfig;
+    private readonly RSA _rsa;
 
-    public CredentialManager(IFido2 fido2, OptionsCache optionsCache)
+    public CredentialManager(IFido2 fido2, OptionsCache optionsCache, IOptions<JwtConfig> jwtOptions)
     {
         _fido2 = fido2;
         _optionsCache = optionsCache;
+        _jwtConfig = jwtOptions.Value;
+        _rsa = ToRsa(_jwtConfig.Key!);
 
         _jsonOptions = new JsonSerializerOptions()
         {
@@ -116,7 +126,7 @@ public class CredentialManager
         return options;
     }
 
-    public async Task<AssertionVerificationResult> VerifyCredential(AuthenticatorAssertionRawResponse assertionResponse)
+    public async Task<string> VerifyCredential(AuthenticatorAssertionRawResponse assertionResponse)
     {
         // 1. Get the assertion options we sent the client
         var optionsWithName = _optionsCache.Get<OptionsWithName<AssertionOptions>>(Convert.ToBase64String(assertionResponse.Id));
@@ -124,9 +134,9 @@ public class CredentialManager
 
         if (File.Exists($"./data/{optionsWithName.Name}.json"))
         {
-            await using var fileStream = File.OpenRead($"./data/{optionsWithName.Name}.json");
+            await using var fileStream1 = File.OpenRead($"./data/{optionsWithName.Name}.json");
             assertionVerification =
-                (AttestationVerificationSuccess)(await JsonSerializer.DeserializeAsync(fileStream,
+                (AttestationVerificationSuccess)(await JsonSerializer.DeserializeAsync(fileStream1,
                     typeof(AttestationVerificationSuccess), _jsonOptions))!;
         }
         else
@@ -147,18 +157,63 @@ public class CredentialManager
         // 5. Make the assertion
         var result = await _fido2.MakeAssertionAsync(assertionResponse, optionsWithName.Options, assertionVerification.PublicKey, storedCounter, Callback);
 
-        if (result.Status == "ok")
+        if (result.Status != "ok")
         {
-            assertionVerification.Counter = result.Counter;
-            await using var fileStream = File.OpenWrite($"./data/{optionsWithName.Name}.json");
-            await JsonSerializer.SerializeAsync(fileStream, assertionVerification, _jsonOptions);
+            throw new Exception($"Verification failed: {result.ErrorMessage}");
         }
+
+        assertionVerification.Counter = result.Counter;
+        await using var fileStream2 = File.OpenWrite($"./data/{optionsWithName.Name}.json");
+        await JsonSerializer.SerializeAsync(fileStream2, assertionVerification, _jsonOptions);
         
+        // var store = new X509Store(StoreName.My);
+        // store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
+        // var collection = store.Certificates.Find(X509FindType.FindByThumbprint, "CE63FA2B6BCD8460150E7FBF9B138E9ED9852E3A", false);
+        // var certificate = collection[0];
+    
+        var token = JwtBuilder.Create()
+            .WithAlgorithm(new RS256Algorithm(_rsa, _rsa))
+            .Issuer("http://localhost:5172")
+            .Audience("http://localhost:5172")
+            .IssuedAt(DateTime.UtcNow)
+            .ExpirationTime(DateTime.UtcNow.AddHours(1))
+            .Subject(optionsWithName.Name)
+            .AddClaim("permissions", new string[] { "MagicClaim", "Foo", "Test" })
+            .AddClaim("scope", "passkey")
+            .Encode();
+
         // TODO: ???
         // if (result.DevicePublicKey is not null)
         //     creds.DevicePublicKeys.Add(result.DevicePublicKey);
 
-        // 7. return OK to client
-        return result;
+        return token;
+    }
+
+    private RSA ToRsa(JsonWebKey key)
+    {
+        var rsaParameters = new RSAParameters
+        {
+            // PUBLIC KEY PARAMETERS
+            // n parameter - public modulus
+            Modulus = Base64UrlEncoder.DecodeBytes(key.N),
+            // e parameter - public exponent
+            Exponent = Base64UrlEncoder.DecodeBytes(key.E),
+    
+            // PRIVATE KEY PARAMETERS (optional)
+            // d parameter - the private exponent value for the RSA key 
+            D = Base64UrlEncoder.DecodeBytes(key.D),
+            // dp parameter - CRT exponent of the first factor
+            DP = Base64UrlEncoder.DecodeBytes(key.DP),
+            // dq parameter - CRT exponent of the second factor
+            DQ = Base64UrlEncoder.DecodeBytes(key.DQ),
+            // p parameter - first prime factor
+            P = Base64UrlEncoder.DecodeBytes(key.P),
+            // q parameter - second prime factor
+            Q = Base64UrlEncoder.DecodeBytes(key.Q),
+            // qi parameter - CRT coefficient of the second factor
+            InverseQ = Base64UrlEncoder.DecodeBytes(key.QI)
+        };
+
+        return RSA.Create(rsaParameters);
     }
 }

+ 8 - 0
JwtConfig.cs

@@ -0,0 +1,8 @@
+using Microsoft.IdentityModel.Tokens;
+
+namespace Passwordless;
+
+public class JwtConfig()
+{
+    public JsonWebKey? Key { get; set; }
+}

+ 1 - 0
Passwordless.csproj

@@ -9,6 +9,7 @@
 
   <ItemGroup>
     <PackageReference Include="Fido2.AspNet" Version="3.0.1" />
+    <PackageReference Include="JWT" Version="10.1.1" />
     <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.1" />
     <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.1" />
     <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />

+ 22 - 3
Program.cs

@@ -4,6 +4,9 @@ using Microsoft.AspNetCore.Mvc;
 using Microsoft.IdentityModel.Tokens;
 using Passwordless;
 
+// TODO: RoslynPad code for key generation
+var jwk = JsonWebKey.Create(File.ReadAllText("./demo-jwk.json"));
+
 var builder = WebApplication.CreateBuilder(args);
 
 // Add services to the container.
@@ -20,10 +23,24 @@ builder.Services.AddFido2(options =>
 builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
     .AddJwtBearer(options =>
     {
+        options.Events = new JwtBearerEvents()
+        {
+            OnAuthenticationFailed = ctx =>
+            {
+                Console.WriteLine(ctx.Exception);
+                return Task.CompletedTask;
+            },
+            OnTokenValidated = ctx =>
+            {
+                Console.WriteLine($"Valid token from {ctx.SecurityToken.Issuer}");
+                return Task.CompletedTask;
+            }
+        };
         options.RequireHttpsMetadata = false; // dev only!!!
         options.Authority = "http://localhost:5172";
         options.TokenValidationParameters = new TokenValidationParameters
         {
+            IssuerSigningKey = jwk,
             ValidIssuer = "http://localhost:5172",
             ValidAudience = "http://localhost:5172"
         };
@@ -34,6 +51,7 @@ builder.Services.AddAuthorization(authorizationOptions =>
     authorizationOptions.AddPolicy("MagicClaim", policyBuilder => policyBuilder.RequireClaim("permissions", "MagicClaim"));
 });
 builder.Services.AddMemoryCache();
+builder.Services.Configure<JwtConfig>(config => config.Key = jwk);
 builder.Services.AddTransient<OptionsCache>();
 builder.Services.AddTransient<CredentialManager>();
 
@@ -66,13 +84,14 @@ app.MapGet("/buildAssertionOptions", async ([FromQuery] string login, Credential
     .WithOpenApi();
 
 app.MapPost("/verifyCredential", async ([FromBody] AuthenticatorAssertionRawResponse assertionResponse, CredentialManager credMan) => 
-    await credMan.VerifyCredential(assertionResponse))
+    Results.Json(await credMan.VerifyCredential(assertionResponse)))
     .WithName("VerifyCredential")
     .WithOpenApi();
 
-app.MapGet("/protected", () => "Success!").WithName("Protected").RequireAuthorization(policy =>
+app.MapGet("/protected", () => Results.Json("Success!")).WithName("Protected").RequireAuthorization(policy =>
 {
-    policy.RequireClaim("MagicClaim");
+    policy.RequireAuthenticatedUser();
+    //policy.RequireClaim("MagicClaim");
 });
 
 app.Run();

+ 1 - 1
data/my username.json

@@ -10,7 +10,7 @@
   "AttestationCertificate": null,
   "AttestationCertificateChain": [],
   "CredentialId": "7M3KnPu2OYqBI83iLPkHyaiuOTX4AsR9qg6fx9hOJdrdfTdk0a3Jd09CjVDJV/qxPTPJFlS02P49Gpp2yewx+g==",
-  "Counter": 17,
+  "Counter": 84,
   "status": null,
   "errorMessage": null
 }

+ 14 - 0
demo-jwk.json

@@ -0,0 +1,14 @@
+{
+  "d": "j8qYP4zsfoiaUNWeZ-dIchUhNLcxZsVUhqOvqZYNWTi-BKRd5YIcdN072p7Z-PctX8P2U5DjiylKotMOM_hEgAt3oMrFI6BPjc9qe8mH6ffPWTlqyFuVvugcj2fvUVNFU1mXf9dEMGo-yA2RNaCay_sQ0Q6AvGCPRSRGok5R28okqcHaOM8HAX4Z8mMUFI3OBMaGma3GfS2gXE82W_Xl-wqw74lQrCK8Xr8tYwsH4LW-UnDa-TAy8vAwHmNvRcn19DC5yAV2VY1GVVaWmzz5wasJc4M5A9nanYmTmI1SQYuu0PSchm5oDgiCW4NlbacIgDvZlDArcqKxJLkFoL_FwQ",
+  "dp": "mo4xI9ZuH3CjZqp_kYxfXaR__Wr8CXTx_13hpQnRZWiiv6jGQDKuOvriuTc7FqHX5DNpfAUbM5682f3dt-0CN620KvJMQOcxFxp0xgYPBfiWfEjSmdu9VRRSNjAnKI-mMivcehtjoYBo12QToBlx40j6UoUBHJriSXEsKZp4Qzc",
+  "dq": "vNeBVKm5SHl2BnWXZbfe8v6cebAORW-jtQg8QZrhJxyAukMZgGaWefFVcAC_Qk9-eJ_L6XIzDyIABcmRABzG1eAItROojrw2EozCsUvujvzXFqU-OZHkCM0Se2oew0Xrkf_U6fd96XzgP42REXCYOMV2NfLqWvC7wGXWg8yKceU",
+  "e": "AQAB",
+  "key_ops": [],
+  "kty": "RSA",
+  "n": "oVHc2Gf3otX31mt8uonIcAme3CLyCaK5Be0GlEEHkc0yQ1LR-dRlNeSWS_wF_ohWNIYpePwaxIa4vkoedj82lorv20TPwH9HF1g-mBphaK9D4Of3l_kewe-2ernOd6HcaMBPfBphxyCr3mXID10eRqJg9LVDJoAWCr5Z63op1aM2C1LPKYT_qyjrEm36O3-dsk7DeLlWyKsW3rd8ALN9UjDRwFuYcJ3pxi-jhKnPdTlcnfNd9lGI_FvkYoSWoqaz_8qXTZ_76RqzJdFmIr-vFqtzAwbEWn3rB3D07RLWFKKrf4oe9y1TnshT9MNmStlmBgZZx3oRN9HMEuuaDUjbOQ",
+  "oth": [],
+  "p": "y8jJSZ9g52PlZlpmmzOTVlMv6eOmQw2EVz83Rgbsor36XfNuvlULn2ezluFfSV8_se_ol_2wSTpXQvlj-7_g3-Ww51j1YYUMx5XhezTqUnnuwC2QnGX-fXDohaJCdDet-ViTFagcyPKdrEUNI7kz5NdgesghwO2nXQ8_jfvmTEc",
+  "q": "yqeeVEUsuFrf1LiaT0qGzp4Q_KPrmZumg48GZ5Hy4RiFogiM7hE_y3MMcFXqEGBzZTo2DB12fn_iHoHLElN7T94ww9SyLtod7NAbwlKlqOgFyF3I-vqD7B5DOTjcHVzuCHtfLBvmni5NgHkQo0G6TXk780LlUnN5KMtAuLmk3H8",
+  "qi": "v81BV7MHrkgEs5AB93EsfHbAPe9juno6jmBDSCNIzwVELFnomeTSgXIAUucueS0tXpzLdQ6L8gm1TtaONcA_MTSz_3to9-1n_z0My5eebqtv-Umz5A10lnGhZK9ulYQgkogclayt3ePu4EoyavXJVR2aFGmWOsyaOScpg4I6vec",
+  "x5c": []
+}

+ 2 - 2
wwwroot/index.html

@@ -15,11 +15,11 @@
     </div>
     <hr />
     <div>
-        <pre><code id="userState">TEST</code></pre>
+        <pre><code id="userState">[not authenticated]</code></pre>
         <div>
             <button id="protected">Access Protected API</button>
         </div>
-        <pre><code id="response">TEST</code></pre>
+        <pre><code id="response">[empty]</code></pre>
     </div>
     <script src="main.js"></script>
 </body>

+ 20 - 7
wwwroot/main.js

@@ -251,11 +251,8 @@ class Login {
 
         const result = await response.json();
 
-        console.log("Assertion Object", result);
-
-        if (result.status !== "ok") {
-            throw new Error(`Error in verifyCredential: ${result.errorMessage}`);
-        }
+        console.log("JWT", result);
+        window.api.setToken(result);
         
         return result;
     }
@@ -264,6 +261,14 @@ class Login {
 class ApiHandler {
     constructor() {
         document.getElementById('protected').addEventListener('click', this.access.bind(this));
+        this.userStateElement = document.getElementById('userState');
+        this.responseElement = document.getElementById('response');
+        this.token = null;
+    }
+    
+    setToken(value) {
+        this.token = value;
+        this.userStateElement.textContent = this.token;
     }
     
     async access() {
@@ -271,12 +276,20 @@ class ApiHandler {
             method: 'GET',
             headers: {
                 'Accept': 'application/json',
-                'Content-Type': 'application/json'
+                'Content-Type': 'application/json',
+                'Authorization': `Bearer ${this.token}`
             }
         });
         
+        const data = response.status === 200 ? await response.json() : await response.text();
+        
+        if (response.status === 200) {
+            this.responseElement.textContent = JSON.stringify(data);
+        } else {
+            this.responseElement.textContent = `${response.status} ${response.statusText} - ${data}`;
+        }
         console.log("/protected returned", response);
-        //response.status
+        console.log("data", data);
     }
 }