Skip to content

Commit adde02d

Browse files
authored
Support cNonce endpoint (#321)
* add cNonce service Signed-off-by: Johannes Tuerk <johannes.tuerk@lissi.id> * write out cNonce to credentialNonce Signed-off-by: Johannes Tuerk <johannes.tuerk@lissi.id> --------- Signed-off-by: Johannes Tuerk <johannes.tuerk@lissi.id>
1 parent cca9fda commit adde02d

File tree

10 files changed

+178
-36
lines changed

10 files changed

+178
-36
lines changed

src/WalletFramework.Oid4Vc/DependencyInjection/ServiceCollectionExtensions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
using WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Abstractions;
1616
using WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Implementations;
1717
using WalletFramework.Oid4Vc.Oid4Vci.Authorization.Implementations;
18+
using WalletFramework.Oid4Vc.Oid4Vci.CredentialNonce.Abstractions;
19+
using WalletFramework.Oid4Vc.Oid4Vci.CredentialNonce.Implementations;
1820
using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Abstractions;
1921
using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Implementations;
2022
using WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Abstractions;
@@ -70,6 +72,7 @@ public static IServiceCollection AddOpenIdServices(this IServiceCollection build
7072
builder.AddSingleton<IStatusListService, StatusListService>();
7173
builder.AddSingleton<ITokenService, TokenService>();
7274
builder.AddSingleton<IVctMetadataService, VctMetadataService>();
75+
builder.AddSingleton<ICredentialNonceService, CredentialNonceService>();
7376

7477
builder.AddSdJwtVcServices();
7578

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
using LanguageExt;
12
using OneOf;
23
using WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Models;
34
using WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models;
5+
using WalletFramework.Oid4Vc.Oid4Vci.CredentialNonce.Models;
46

57
namespace WalletFramework.Oid4Vc.Oid4Vci.Authorization.Abstractions;
68

79
public interface ITokenService
810
{
911
public Task<OneOf<OAuthToken, DPopToken>> RequestToken(
1012
TokenRequest tokenRequest,
11-
AuthorizationServerMetadata metadata);
13+
AuthorizationServerMetadata metadata,
14+
Option<CredentialNonceEndpoint> credentialNonceEndpoint);
1215
}

src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Implementations/TokenService.cs

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,56 @@
1+
using LanguageExt;
12
using OneOf;
23
using WalletFramework.Core.Cryptography.Abstractions;
34
using WalletFramework.Oid4Vc.Oid4Vci.Authorization.Abstractions;
45
using WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Abstractions;
56
using WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Models;
67
using WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models;
8+
using WalletFramework.Oid4Vc.Oid4Vci.CredentialNonce.Abstractions;
9+
using WalletFramework.Oid4Vc.Oid4Vci.CredentialNonce.Models;
710
using static Newtonsoft.Json.JsonConvert;
811

912
namespace WalletFramework.Oid4Vc.Oid4Vci.Authorization.Implementations;
1013

11-
internal class TokenService : ITokenService
14+
internal class TokenService(
15+
IDPopHttpClient dPopHttpClient,
16+
IHttpClientFactory httpClientFactory,
17+
ICredentialNonceService credentialNonceService,
18+
IKeyStore keyStore)
19+
: ITokenService
1220
{
13-
private readonly IDPopHttpClient _dPopHttpClient;
14-
private readonly IKeyStore _keyStore;
15-
private readonly HttpClient _httpClient;
21+
private readonly HttpClient _httpClient = httpClientFactory.CreateClient();
1622

17-
public TokenService(
18-
IDPopHttpClient dPopHttpClient,
19-
IHttpClientFactory httpClientFactory,
20-
IKeyStore keyStore)
21-
{
22-
_dPopHttpClient = dPopHttpClient;
23-
_keyStore = keyStore;
24-
_httpClient = httpClientFactory.CreateClient();
25-
}
26-
2723
public async Task<OneOf<OAuthToken, DPopToken>> RequestToken(
2824
TokenRequest tokenRequest,
29-
AuthorizationServerMetadata metadata)
25+
AuthorizationServerMetadata metadata,
26+
Option<CredentialNonceEndpoint> credentialNonceEndpoint)
3027
{
3128
if (metadata.IsDPoPSupported)
3229
{
33-
var keyId = await _keyStore.GenerateKey(isPermanent: false);
30+
var keyId = await keyStore.GenerateKey(isPermanent: false);
3431

3532
var config = new DPopConfig(keyId, metadata.TokenEndpoint);
3633

3734
var uri = new Uri(metadata.TokenEndpoint);
3835

39-
var result = await _dPopHttpClient.Post(
36+
var result = await dPopHttpClient.Post(
4037
uri,
4138
config,
4239
tokenRequest.ToFormUrlEncoded);
4340

4441
var token = DeserializeObject<OAuthToken>(await result.ResponseMessage.Content.ReadAsStringAsync())
4542
?? throw new InvalidOperationException("Failed to deserialize the token response");
4643

47-
var dPop = new DPop.Models.DPop(result.Config);
44+
await credentialNonceEndpoint.IfSomeAsync(async endpoint =>
45+
{
46+
var credentialNonce = await credentialNonceService.GetCredentialNonce(endpoint);
47+
token = token with { CNonce = credentialNonce.Value };
48+
});
4849

49-
return new DPopToken(token, dPop);
50+
return new DPopToken(
51+
token,
52+
new DPop.Models.DPop(result.Config)
53+
);
5054
}
5155
else
5256
{
@@ -56,7 +60,13 @@ public async Task<OneOf<OAuthToken, DPopToken>> RequestToken(
5660

5761
var token = DeserializeObject<OAuthToken>(await response.Content.ReadAsStringAsync())
5862
?? throw new InvalidOperationException("Failed to deserialize the token response");
59-
63+
64+
await credentialNonceEndpoint.IfSomeAsync(async endpoint =>
65+
{
66+
var credentialNonce = await credentialNonceService.GetCredentialNonce(endpoint);
67+
token = token with { CNonce = credentialNonce.Value };
68+
});
69+
6070
return token;
6171
}
6272
}

src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/OAuthToken.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public record OAuthToken
4646
/// when requesting a Credential.
4747
/// </summary>
4848
[JsonProperty("c_nonce")]
49-
public string CNonce { get; set; }
49+
public string? CNonce { get; set; }
5050

5151
/// <summary>
5252
/// Gets or sets the refresh token, which can be used to obtain new access tokens.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using WalletFramework.Oid4Vc.Oid4Vci.CredentialNonce.Models;
2+
3+
namespace WalletFramework.Oid4Vc.Oid4Vci.CredentialNonce.Abstractions;
4+
5+
public interface ICredentialNonceService
6+
{
7+
Task<Models.CredentialNonce> GetCredentialNonce(CredentialNonceEndpoint credentialNonceEndpoint);
8+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using Newtonsoft.Json.Linq;
2+
using WalletFramework.Core.Functional;
3+
using WalletFramework.Core.Json;
4+
using WalletFramework.Oid4Vc.Oid4Vci.CredentialNonce.Abstractions;
5+
using WalletFramework.Oid4Vc.Oid4Vci.CredentialNonce.Models;
6+
7+
namespace WalletFramework.Oid4Vc.Oid4Vci.CredentialNonce.Implementations;
8+
9+
public class CredentialNonceService(IHttpClientFactory httpClientFactory) : ICredentialNonceService
10+
{
11+
public async Task<Models.CredentialNonce> GetCredentialNonce(CredentialNonceEndpoint credentialNonceEndpoint)
12+
{
13+
var client = httpClientFactory.CreateClient();
14+
var response = await client.PostAsync(credentialNonceEndpoint.Value, new StringContent(""));
15+
16+
var message = await response.Content.ReadAsStringAsync();
17+
18+
if (!response.IsSuccessStatusCode)
19+
throw new HttpRequestException($"Requesting the c_nonce failed. Status Code is {response.StatusCode} with message: {message}");
20+
21+
return (from jToken in JObject.Parse(message).GetByKey("c_nonce")
22+
from docType in Models.CredentialNonce.ValidCredentialNonce(jToken.ToString())
23+
select docType)
24+
.Match(
25+
nonce => nonce,
26+
_ => throw new InvalidOperationException("Failed deserialize c_nonce from nonce endpoint response"));
27+
}
28+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using WalletFramework.Core.Functional;
2+
using WalletFramework.Core.Functional.Errors;
3+
4+
namespace WalletFramework.Oid4Vc.Oid4Vci.CredentialNonce.Models;
5+
6+
public readonly struct CredentialNonce
7+
{
8+
public string Value { get; }
9+
10+
private CredentialNonce(string nonce)
11+
{
12+
Value = nonce;
13+
}
14+
15+
public static Validation<CredentialNonce> ValidCredentialNonce(string nonce)
16+
{
17+
if (string.IsNullOrEmpty(nonce))
18+
return new StringIsNullOrWhitespaceError<CredentialNonce>();
19+
20+
return new CredentialNonce(nonce);
21+
}
22+
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace WalletFramework.Oid4Vc.Oid4Vci.CredentialNonce.Models;
2+
3+
public readonly struct CredentialNonceEndpoint(Uri endpoint)
4+
{
5+
public Uri Value { get; } = endpoint;
6+
};

src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
using WalletFramework.Oid4Vc.CredentialSet;
2424
using WalletFramework.Oid4Vc.CredentialSet.Models;
2525
using WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Models;
26+
using WalletFramework.Oid4Vc.Oid4Vci.CredentialNonce.Abstractions;
2627
using WalletFramework.Oid4Vc.Oid4Vci.CredResponse;
2728
using WalletFramework.Oid4Vc.Oid4Vp.Models;
2829
using WalletFramework.SdJwtVc.Models;
@@ -63,6 +64,7 @@ public Oid4VciClientService(
6364
IIssuerMetadataService issuerMetadataService,
6465
IMdocStorage mdocStorage,
6566
ISdJwtVcHolderService sdJwtService,
67+
ICredentialNonceService credentialNonceService,
6668
ITokenService tokenService)
6769
{
6870
_agentProvider = agentProvider;
@@ -74,6 +76,7 @@ public Oid4VciClientService(
7476
_issuerMetadataService = issuerMetadataService;
7577
_mdocStorage = mdocStorage;
7678
_sdJwtService = sdJwtService;
79+
_credentialNonceService = credentialNonceService;
7780
_tokenService = tokenService;
7881
}
7982

@@ -86,6 +89,7 @@ public Oid4VciClientService(
8689
private readonly IIssuerMetadataService _issuerMetadataService;
8790
private readonly IMdocStorage _mdocStorage;
8891
private readonly ISdJwtVcHolderService _sdJwtService;
92+
private readonly ICredentialNonceService _credentialNonceService;
8993
private readonly ITokenService _tokenService;
9094

9195
/// <inheritdoc />
@@ -269,7 +273,8 @@ from preAuthCode in grants.PreAuthorizedCode
269273

270274
var token = await _tokenService.RequestToken(
271275
tokenRequest,
272-
authorizationServerMetadata);
276+
authorizationServerMetadata,
277+
issuerMetadata.CredentialNonceEndpoint);
273278

274279
var validResponse = await _credentialRequestService.RequestCredentials(
275280
configuration,
@@ -377,8 +382,9 @@ public async Task<Validation<CredentialSetRecord>> RequestCredentialSet(Issuance
377382

378383
var token = await _tokenService.RequestToken(
379384
tokenRequest,
380-
session.AuthorizationData.AuthorizationServerMetadata);
381-
385+
session.AuthorizationData.AuthorizationServerMetadata,
386+
session.AuthorizationData.IssuerMetadata.CredentialNonceEndpoint);
387+
382388
var credentialSet = new CredentialSetRecord();
383389

384390
//TODO: Make sure that it does not always request all available credConfigurations
@@ -403,9 +409,26 @@ select credentialsOrTransactionId.Match(
403409
await credential.Value.Match(
404410
async sdJwt =>
405411
{
406-
token = token.Match<OneOf<OAuthToken, DPopToken>>(
407-
oAuth => oAuth with { CNonce = cNonce.ToNullable() },
408-
dPop => dPop with { Token = dPop.Token with { CNonce = cNonce.ToNullable() } });
412+
token = await session.AuthorizationData.IssuerMetadata.CredentialNonceEndpoint.Match(
413+
Some: async credentialNonceEndpoint =>
414+
{
415+
var credentialNonce = await _credentialNonceService.GetCredentialNonce(credentialNonceEndpoint);
416+
return token.Match<OneOf<OAuthToken, DPopToken>>(
417+
oAuth => oAuth with { CNonce = credentialNonce.Value },
418+
dPop => dPop with
419+
{
420+
Token = dPop.Token with { CNonce = credentialNonce.Value }
421+
});
422+
},
423+
None: () =>
424+
{
425+
return Task.FromResult<OneOf<OAuthToken, DPopToken>>( token.Match<OneOf<OAuthToken, DPopToken>>(
426+
oAuth => oAuth with { CNonce = cNonce.ToNullable() },
427+
dPop => dPop with
428+
{
429+
Token = dPop.Token with { CNonce = cNonce.ToNullable() }
430+
}));
431+
});
409432

410433
var record = sdJwt.Decoded.ToRecord(
411434
configuration.AsT0,
@@ -419,9 +442,26 @@ await credential.Value.Match(
419442
},
420443
async mdoc =>
421444
{
422-
token = token.Match<OneOf<OAuthToken, DPopToken>>(
423-
oAuth => oAuth with { CNonce = cNonce.ToNullable() },
424-
dPop => dPop with { Token = dPop.Token with { CNonce = cNonce.ToNullable() } });
445+
token = await session.AuthorizationData.IssuerMetadata.CredentialNonceEndpoint.Match(
446+
Some: async credentialNonceEndpoint =>
447+
{
448+
var credentialNonce = await _credentialNonceService.GetCredentialNonce(credentialNonceEndpoint);
449+
return token.Match<OneOf<OAuthToken, DPopToken>>(
450+
oAuth => oAuth with { CNonce = credentialNonce.Value },
451+
dPop => dPop with
452+
{
453+
Token = dPop.Token with { CNonce = credentialNonce.Value }
454+
});
455+
},
456+
None: () =>
457+
{
458+
return Task.FromResult<OneOf<OAuthToken, DPopToken>>( token.Match<OneOf<OAuthToken, DPopToken>>(
459+
oAuth => oAuth with { CNonce = cNonce.ToNullable() },
460+
dPop => dPop with
461+
{
462+
Token = dPop.Token with { CNonce = cNonce.ToNullable() }
463+
}));
464+
});
425465

426466
var displays = MdocFun.CreateMdocDisplays(configuration.AsT1);
427467

@@ -484,7 +524,8 @@ public async Task<Validation<OnDemandCredentialSet>> RequestOnDemandCredentialSe
484524

485525
var token = await _tokenService.RequestToken(
486526
tokenRequest,
487-
session.AuthorizationData.AuthorizationServerMetadata);
527+
session.AuthorizationData.AuthorizationServerMetadata,
528+
session.AuthorizationData.IssuerMetadata.CredentialNonceEndpoint);
488529

489530
var credentialConfigs = credentialType.Match(
490531
vct => credConfigurations.Where(config => config.Match(

0 commit comments

Comments
 (0)