From 0934b40837b2d7f28cae3c3e0b295dcaaa0245a2 Mon Sep 17 00:00:00 2001 From: Johannes Tuerk Date: Tue, 29 Apr 2025 08:48:53 +0200 Subject: [PATCH 1/6] Set MaxBatchSize to 10 Signed-off-by: Johannes Tuerk --- .../CredRequest/Implementations/CredentialRequestService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs index 13aafd3e..cdd912d4 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs @@ -28,7 +28,7 @@ namespace WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Implementations; public class CredentialRequestService : ICredentialRequestService { - private const int MaxBatchSize = 20; + private const int MaxBatchSize = 10; public CredentialRequestService( HttpClient httpClient, From 0d3c67597b505ea50685de17014d9679c2e3a4c2 Mon Sep 17 00:00:00 2001 From: Johannes Tuerk Date: Mon, 19 May 2025 11:43:07 +0200 Subject: [PATCH 2/6] add support for credential identifiers Signed-off-by: Johannes Tuerk --- .../Abstractions/IOid4VciClientService.cs | 7 +- .../Abstractions/IAuthFlowSessionStorage.cs | 4 +- .../Implementations/AuthFlowSessionStorage.cs | 6 +- .../AuthFlow/Models/AuthorizationDetails.cs | 20 + .../AuthFlow/Models/CredentialIdentifier.cs | 10 + .../AuthFlow/Records/AuthFlowSessionRecord.cs | 20 +- .../Authorization/Models/OAuthToken.cs | 4 +- .../Abstractions/ICredentialRequestService.cs | 11 +- .../CredentialRequestService.cs | 151 +++--- .../CredRequest/Models/CredentialRequest.cs | 30 +- .../Implementations/Oid4VciClientService.cs | 433 +++++++++--------- .../AuthFlow/AuthFlowSessionRecordTests.cs | 2 +- 12 files changed, 395 insertions(+), 303 deletions(-) create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/CredentialIdentifier.cs diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs index 5098f2b9..335682d3 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs @@ -30,8 +30,9 @@ public interface IOid4VciClientService /// The issuers uri /// The client options /// Optional language tag + /// Optional language tag /// - Task InitiateAuthFlow(Uri uri, ClientOptions clientOptions, Option language); + Task InitiateAuthFlow(Uri uri, ClientOptions clientOptions, Option language, int specVersion); /// /// Requests a verifiable credential using the authorization code flow. @@ -40,7 +41,7 @@ public interface IOid4VciClientService /// /// A list of credentials. /// - Task> RequestCredentialSet(IssuanceSession issuanceSession); + Task>> RequestCredentialSet(IssuanceSession issuanceSession); /// /// Requests a verifiable credential using the authorization code flow and C''. @@ -66,7 +67,7 @@ public interface IOid4VciClientService /// /// /// Credential offer and Issuer Metadata /// The Transaction Code. - Task> AcceptOffer( + Task>> AcceptOffer( CredentialOfferMetadata credentialOfferMetadata, string? transactionCode); } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Abstractions/IAuthFlowSessionStorage.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Abstractions/IAuthFlowSessionStorage.cs index 37126b65..0f2e490d 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Abstractions/IAuthFlowSessionStorage.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Abstractions/IAuthFlowSessionStorage.cs @@ -44,10 +44,12 @@ public interface IAuthFlowSessionStorage /// flow. /// /// Session State Identifier of a Authorization Code Flow session + /// Session State Identifier of a Authorization Code Flow session /// Task StoreAsync( IAgentContext agentContext, AuthorizationData authorizationData, AuthorizationCodeParameters authorizationCodeParameters, - AuthFlowSessionState authFlowSessionState); + AuthFlowSessionState authFlowSessionState, + int specVersion = 15); } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Implementations/AuthFlowSessionStorage.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Implementations/AuthFlowSessionStorage.cs index 0f0862e4..8e72ba8a 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Implementations/AuthFlowSessionStorage.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Implementations/AuthFlowSessionStorage.cs @@ -26,12 +26,14 @@ public async Task StoreAsync( IAgentContext agentContext, AuthorizationData authorizationData, AuthorizationCodeParameters authorizationCodeParameters, - AuthFlowSessionState authFlowSessionState) + AuthFlowSessionState authFlowSessionState, + int specVersion) { var record = new AuthFlowSessionRecord( authorizationData, authorizationCodeParameters, - authFlowSessionState); + authFlowSessionState, + specVersion); await _recordService.AddAsync(agentContext.Wallet, record); diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/AuthorizationDetails.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/AuthorizationDetails.cs index a38aebf3..a18aac26 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/AuthorizationDetails.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/AuthorizationDetails.cs @@ -36,6 +36,9 @@ public record AuthorizationDetails [JsonProperty("locations", NullValueHandling = NullValueHandling.Ignore)] public string[]? Locations { get; } + + [JsonProperty("credential_identifiers", NullValueHandling = NullValueHandling.Ignore)] + public string[]? CredentialIdentifiers { get; } internal AuthorizationDetails( string credentialConfigurationId, @@ -44,4 +47,21 @@ internal AuthorizationDetails( CredentialConfigurationId = credentialConfigurationId; Locations = locations; } + + [JsonConstructor] + private AuthorizationDetails( + string format, + string? vct, + string? docType, + string? credentialConfigurationId, + string[]? locations, + string[]? credentialIdentifiers) + { + Format = format; + Vct = vct; + DocType = docType; + CredentialConfigurationId = credentialConfigurationId; + Locations = locations; + CredentialIdentifiers = credentialIdentifiers; + } } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/CredentialIdentifier.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/CredentialIdentifier.cs new file mode 100644 index 00000000..4b45839a --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/CredentialIdentifier.cs @@ -0,0 +1,10 @@ +namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; + +public struct CredentialIdentifier +{ + private string Value { get; } + + public override string ToString() => Value; + + public CredentialIdentifier(string id) => Value = id; +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Records/AuthFlowSessionRecord.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Records/AuthFlowSessionRecord.cs index a1f30722..805dcbd4 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Records/AuthFlowSessionRecord.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Records/AuthFlowSessionRecord.cs @@ -37,6 +37,11 @@ public AuthFlowSessionState AuthFlowSessionState /// The parameters for the 'authorization_code' grant type. /// public AuthorizationCodeParameters AuthorizationCodeParameters { get; } + + /// + /// Used to track the VCI Specification verison + /// + public int SpecVersion { get; } /// /// Initializes a new instance of the class. @@ -58,15 +63,18 @@ public AuthFlowSessionRecord() /// /// /// + /// public AuthFlowSessionRecord( AuthorizationData authorizationData, AuthorizationCodeParameters authorizationCodeParameters, - AuthFlowSessionState authFlowSessionState) + AuthFlowSessionState authFlowSessionState, + int specVersion) { AuthFlowSessionState = authFlowSessionState; RecordVersion = 1; AuthorizationCodeParameters = authorizationCodeParameters; AuthorizationData = authorizationData; + SpecVersion = specVersion; } } @@ -94,6 +102,7 @@ public static class AuthFlowSessionRecordFun { private const string AuthorizationDataJsonKey = "authorization_data"; private const string AuthorizationCodeParametersJsonKey = "authorization_code_parameters"; + private const string SpecVersionJsonKey = "spec_version"; public static JObject EncodeToJson(this AuthFlowSessionRecord record) { @@ -104,7 +113,8 @@ public static JObject EncodeToJson(this AuthFlowSessionRecord record) { { nameof(RecordBase.Id), record.Id }, { AuthorizationDataJsonKey, authorizationData }, - { AuthorizationCodeParametersJsonKey, authorizationCodeParameters } + { AuthorizationCodeParametersJsonKey, authorizationCodeParameters }, + { SpecVersionJsonKey, record.SpecVersion } }; } @@ -119,8 +129,10 @@ public static AuthFlowSessionRecord DecodeFromJson(JObject json) var authorizationData = AuthorizationDataFun .DecodeFromJson(json[AuthorizationDataJsonKey]!.ToObject()!); - - var result = new AuthFlowSessionRecord(authorizationData, authCodeParameters!, id); + + var specVersion = json[SpecVersionJsonKey]!.ToObject(); + + var result = new AuthFlowSessionRecord(authorizationData, authCodeParameters!, id, specVersion); return result; } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/OAuthToken.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/OAuthToken.cs index ad912627..a50c21bc 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/OAuthToken.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/OAuthToken.cs @@ -69,6 +69,6 @@ public record OAuthToken /// /// Gets or sets the credential identifier. /// - [JsonProperty("credential_identifiers")] - public AuthorizationDetails? CredentialIdentifier { get; set; } + [JsonProperty("authorization_details", NullValueHandling = NullValueHandling.Ignore)] + public AuthorizationDetails[]? AuthorizationDetails { get; set; } } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Abstractions/ICredentialRequestService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Abstractions/ICredentialRequestService.cs index b159396c..e9ff7e48 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Abstractions/ICredentialRequestService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Abstractions/ICredentialRequestService.cs @@ -4,8 +4,8 @@ using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; using WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Models; using WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models; -using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc; -using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.SdJwt; +using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; +using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models; using WalletFramework.Oid4Vc.Oid4Vci.CredResponse; using WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models; using WalletFramework.Oid4Vc.Oid4Vp.Models; @@ -14,10 +14,11 @@ namespace WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Abstractions; public interface ICredentialRequestService { - public Task> RequestCredentials( - OneOf configuration, + public Task>> RequestCredentials( + KeyValuePair configurationPair, IssuerMetadata issuerMetadata, OneOf token, Option clientOptions, - Option authorizationRequest); + Option authorizationRequest, + int specVersion = 15); } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs index cdd912d4..edfdb646 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs @@ -12,8 +12,7 @@ using WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Models; using WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models; using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; -using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc; -using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.SdJwt; +using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models; using WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Abstractions; using WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Models; using WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Models.Mdoc; @@ -50,7 +49,9 @@ public CredentialRequestService( private async Task CreateCredentialRequest( KeyId keyId, Format format, + OneOf credentialIdentification, OneOf token, + int specVersion, IssuerMetadata issuerMetadata, Option clientOptions, Option authorizationRequest) @@ -90,8 +91,8 @@ await batchCredentialIssuance.BatchSize.Match( None: async () => proof = await GetProofOfPossessionAsync(keyId, issuerMetadata, cNonce, clientOptions)); }); - - return new CredentialRequest(format, proof, proofs, sessionTranscript); + + return new CredentialRequest(credentialIdentification, format, specVersion, proof, proofs, sessionTranscript); } private async Task GetProofOfPossessionAsync(KeyId keyId, IssuerMetadata issuerMetadata, string cNonce, Option clientOptions) @@ -132,74 +133,92 @@ private async Task GenerateKbProofOfPossession( Option.None); } - async Task> ICredentialRequestService.RequestCredentials( - OneOf configuration, + async Task>> ICredentialRequestService.RequestCredentials( + KeyValuePair configurationPair, IssuerMetadata issuerMetadata, OneOf token, Option clientOptions, - Option authorizationRequest) + Option authorizationRequest, + int specVersion) { - var keyId = await _keyStore.GenerateKey(isPermanent: authorizationRequest.IsNone); - - var requestJson = await configuration.Match( - async sdJwt => - { - var vciRequest = await CreateCredentialRequest( - keyId, - sdJwt.Format, - token, - issuerMetadata, - clientOptions, - authorizationRequest); - - var result = new SdJwtCredentialRequest(vciRequest, sdJwt.Vct); - return result.EncodeToJson(); - }, - async mdoc => - { - var vciRequest = await CreateCredentialRequest( - keyId, - mdoc.Format, - token, - issuerMetadata, - clientOptions, - authorizationRequest); - - var result = new MdocCredentialRequest(vciRequest, mdoc); - return result.EncodeToJson(); - } - ); - - var content = new StringContent( - requestJson, - Encoding.UTF8, - "application/json"); + var credentialIdentifications = + token.Match( + oauthToken => oauthToken.AuthorizationDetails?.First().CredentialIdentifiers, + dPopToken => dPopToken.Token.AuthorizationDetails?.First().CredentialIdentifiers)? + .Select(identifier => (OneOf) new CredentialIdentifier(identifier)) + ?? new List>() { configurationPair.Key }; + + var responses = new List>(); + foreach (var credentialIdentification in credentialIdentifications) + { + var keyId = await _keyStore.GenerateKey(isPermanent: authorizationRequest.IsNone); - var response = await token.Match( - async authToken => await _httpClient - .WithAuthorizationHeader(authToken) - .PostAsync(issuerMetadata.CredentialEndpoint, content), - async dPopToken => - { - var config = dPopToken.DPop.Config with + var requestJson = await configurationPair.Value.Match( + async sdJwt => { - Audience = issuerMetadata.CredentialEndpoint.ToStringWithoutTrail(), - OAuthToken = dPopToken.Token - }; - - var dPopResponse = await _dPopHttpClient.Post( - issuerMetadata.CredentialEndpoint, - config, - () => content); - - return dPopResponse.ResponseMessage; - }); - - var responseContent = await response.Content.ReadAsStringAsync(); + var vciRequest = await CreateCredentialRequest( + keyId, + sdJwt.Format, + credentialIdentification, + token, + specVersion, + issuerMetadata, + clientOptions, + authorizationRequest); + + var result = new SdJwtCredentialRequest(vciRequest, sdJwt.Vct); + return result.EncodeToJson(); + }, + async mdoc => + { + var vciRequest = await CreateCredentialRequest( + keyId, + mdoc.Format, + credentialIdentification, + token, + specVersion, + issuerMetadata, + clientOptions, + authorizationRequest); + + var result = new MdocCredentialRequest(vciRequest, mdoc); + return result.EncodeToJson(); + } + ); + + var content = new StringContent( + requestJson, + Encoding.UTF8, + "application/json"); + + var response = await token.Match( + async authToken => await _httpClient + .WithAuthorizationHeader(authToken) + .PostAsync(issuerMetadata.CredentialEndpoint, content), + async dPopToken => + { + var config = dPopToken.DPop.Config with + { + Audience = issuerMetadata.CredentialEndpoint.ToStringWithoutTrail(), + OAuthToken = dPopToken.Token + }; + + var dPopResponse = await _dPopHttpClient.Post( + issuerMetadata.CredentialEndpoint, + config, + () => content); + + return dPopResponse.ResponseMessage; + }); + + var credentialResponse = + from jObject in JsonFun.ParseAsJObject(await response.Content.ReadAsStringAsync()) + from credResponse in CredentialResponse.ValidCredentialResponse(jObject, keyId) + select credResponse; + + responses.Add(credentialResponse); + } - return - from jObject in JsonFun.ParseAsJObject(responseContent) - from credResponse in CredentialResponse.ValidCredentialResponse(jObject, keyId) - select credResponse; + return responses.TraverseAll(item => item); } } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/CredentialRequest.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/CredentialRequest.cs index 55702448..d4d4117e 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/CredentialRequest.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/CredentialRequest.cs @@ -1,8 +1,11 @@ using LanguageExt; using Newtonsoft.Json.Linq; +using OneOf; using WalletFramework.Core.Base64Url; using WalletFramework.MdocLib.Security; +using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; +using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models; namespace WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Models; @@ -11,7 +14,7 @@ namespace WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Models; /// This request contains the format of the credential, the type of credential, /// and a proof of possession of the key material the issued credential shall be bound to. /// -public record CredentialRequest(Format Format, Option Proof, Option Proofs, Option SessionTranscript) +public record CredentialRequest(OneOf CredentialIdentification, Format Format, int specVersion, Option Proof, Option Proofs, Option SessionTranscript) { /// /// Gets the proof of possession of the key material the issued credential shall be bound to. @@ -23,12 +26,17 @@ public record CredentialRequest(Format Format, Option Proof, /// public Option Proofs { get; } = Proofs; + //TODO: Remove when backward compatibility is not needed anymore /// /// Gets the format of the credential to be issued. /// public Format Format { get; } = Format; public Option SessionTranscript { get; } = SessionTranscript; + + public OneOf CredentialIdentification { get; } = CredentialIdentification; + + public int SpecVersion { get; } = specVersion; } public static class CredentialRequestFun @@ -37,6 +45,8 @@ public static class CredentialRequestFun private const string ProofsJsonKey = "proofs"; private const string FormatJsonKey = "format"; private const string SessionTranscriptKey = "session_transcript"; + private const string CredentialIdentifierKey = "credential_identifier"; + private const string CredentialConfigurationIdKey = "credential_configuration_id"; public static JObject EncodeToJson(this CredentialRequest request) { @@ -57,8 +67,22 @@ public static JObject EncodeToJson(this CredentialRequest request) result.Add(SessionTranscriptKey, Base64UrlString.CreateBase64UrlString(sessionTranscript.ToCbor().ToJSONBytes()).ToString()); }); - result.Add(FormatJsonKey, request.Format.ToString()); - + request.CredentialIdentification.Match( + identifier => + { + result.Add(CredentialIdentifierKey, identifier.ToString()); + return Unit.Default; + }, + configurationId => + { + if (request.SpecVersion == 15) + result.Add(CredentialConfigurationIdKey, configurationId.ToString()); + else + result.Add(FormatJsonKey, request.Format.ToString()); + + return Unit.Default; + }); + return result; } } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs index cd0235ff..b00be653 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs @@ -169,7 +169,7 @@ await _authFlowSessionStorage.StoreAsync( return authorizationRequestUri; } - public async Task InitiateAuthFlow(Uri uri, ClientOptions clientOptions, Option language) + public async Task InitiateAuthFlow(Uri uri, ClientOptions clientOptions, Option language, int specVersion) { var locale = language.Match( some => some, @@ -227,7 +227,8 @@ await _authFlowSessionStorage.StoreAsync( context, authorizationData, authorizationCodeParameters, - sessionId); + sessionId, + specVersion); return authorizationRequestUri; }, @@ -251,12 +252,10 @@ private async Task GetRequestUriUsingPushedAuthorizationRequest(Authorizati + "&request_uri=" + System.Net.WebUtility.UrlEncode(parResponse.RequestUri.ToString())); } - public async Task> AcceptOffer(CredentialOfferMetadata credentialOfferMetadata, string? transactionCode) + public async Task>> AcceptOffer(CredentialOfferMetadata credentialOfferMetadata, string? transactionCode) { var issuerMetadata = credentialOfferMetadata.IssuerMetadata; - // TODO: Support multiple configs - var configId = credentialOfferMetadata.CredentialOffer.CredentialConfigurationIds.First(); - var configuration = issuerMetadata.CredentialConfigurationsSupported[configId]; + var preAuthorizedCode = from grants in credentialOfferMetadata.CredentialOffer.Grants from preAuthCode in grants.PreAuthorizedCode @@ -276,63 +275,71 @@ from preAuthCode in grants.PreAuthorizedCode authorizationServerMetadata, issuerMetadata.CredentialNonceEndpoint); - var validResponse = await _credentialRequestService.RequestCredentials( - configuration, + // TODO: Support multiple configs + var configurationId = credentialOfferMetadata.CredentialOffer.CredentialConfigurationIds.First(); + var configurationPair = issuerMetadata.CredentialConfigurationsSupported.Single(config => config.Key == configurationId); + + var validResponses = await _credentialRequestService.RequestCredentials( + configurationPair, issuerMetadata, token, Option.None, Option.None); - - var credentialSet = new CredentialSetRecord(); - + + var credentialSets = new List(); var result = - from response in validResponse - let credentialsOrTransactionId = response.CredentialsOrTransactionId - select credentialsOrTransactionId.Match( - async creds => - { - foreach (var credential in creds) + from responses in validResponses + let credentialSet = new CredentialSetRecord() + select + from response in responses + let credentialsOrTransactionId = response.CredentialsOrTransactionId + select credentialsOrTransactionId.Match( + async creds => { - await credential.Value.Match( - async sdJwt => - { - var record = sdJwt.Decoded.ToRecord( - configuration.AsT0, - response.KeyId, - credentialSet.CredentialSetId, - creds.Count > 1); - - var context = await _agentProvider.GetContextAsync(); - await _sdJwtService.AddAsync(context, record); + foreach (var credential in creds) + { + await credential.Value.Match( + async sdJwt => + { + var record = sdJwt.Decoded.ToRecord( + configurationPair.Value.AsT0, + response.KeyId, + credentialSet.CredentialSetId, + creds.Count > 1); + + var context = await _agentProvider.GetContextAsync(); + await _sdJwtService.AddAsync(context, record); - credentialSet.AddSdJwtData(record); - }, - async mdoc => - { - var displays = MdocFun.CreateMdocDisplays(configuration.AsT1); - - var record = mdoc.Decoded.ToRecord( - displays, - response.KeyId, - credentialSet.CredentialSetId, - creds.Count > 1); - - await _mdocStorage.Add(record); + credentialSet.AddSdJwtData(record); + }, + async mdoc => + { + var displays = MdocFun.CreateMdocDisplays(configurationPair.Value.AsT1); + + var record = mdoc.Decoded.ToRecord( + displays, + response.KeyId, + credentialSet.CredentialSetId, + creds.Count > 1); + + await _mdocStorage.Add(record); - credentialSet.AddMDocData(record, issuerMetadata.CredentialIssuer); - }); - } - }, - // ReSharper disable once UnusedParameter.Local - transactionId => throw new NotImplementedException()); + credentialSet.AddMDocData(record, issuerMetadata.CredentialIssuer); + }); + } + credentialSets.Add(credentialSet); + }, + // ReSharper disable once UnusedParameter.Local + transactionId => throw new NotImplementedException()); - await result.OnSuccess(async task => + await result.OnSuccess(async tasks => await Task.WhenAll(tasks)); + + foreach (var credentialSet in credentialSets) { - await task; await _credentialSetStorage.Add(credentialSet); - }); - - return credentialSet; + } + + return credentialSets; } public async Task> ProcessOffer(Uri credentialOffer, Option language) @@ -350,7 +357,7 @@ from metadata in _issuerMetadataService.ProcessMetadata(offer.CredentialIssuer, } /// - public async Task> RequestCredentialSet(IssuanceSession issuanceSession) + public async Task>> RequestCredentialSet(IssuanceSession issuanceSession) { var context = await _agentProvider.GetContextAsync(); @@ -360,8 +367,7 @@ public async Task> RequestCredentialSet(Issuance .AuthorizationData .IssuerMetadata .CredentialConfigurationsSupported - .Where(config => session.AuthorizationData.CredentialConfigurationIds.Contains(config.Key)) - .Select(pair => pair.Value); + .Where(config => session.AuthorizationData.CredentialConfigurationIds.Contains(config.Key)); var scope = session .AuthorizationData @@ -385,109 +391,96 @@ public async Task> RequestCredentialSet(Issuance session.AuthorizationData.AuthorizationServerMetadata, session.AuthorizationData.IssuerMetadata.CredentialNonceEndpoint); - var credentialSet = new CredentialSetRecord(); - //TODO: Make sure that it does not always request all available credConfigurations + var credentialSets = new List(); foreach (var configuration in credConfiguration) { - var validResponse = await _credentialRequestService.RequestCredentials( + var validResponses = await _credentialRequestService.RequestCredentials( configuration, session.AuthorizationData.IssuerMetadata, token, session.AuthorizationData.ClientOptions, - Option.None); + Option.None, + session.SpecVersion); var result = - from response in validResponse - let cNonce = response.CNonce - let credentialsOrTransactionId = response.CredentialsOrTransactionId - select credentialsOrTransactionId.Match( - async creds => - { - foreach (var credential in creds) + from responses in validResponses + let credentialSet = new CredentialSetRecord() + select + from response in responses + let cNonce = response.CNonce + let credentialsOrTransactionId = response.CredentialsOrTransactionId + select credentialsOrTransactionId.Match( + async creds => { - await credential.Value.Match( - async sdJwt => + token = await session.AuthorizationData.IssuerMetadata.CredentialNonceEndpoint.Match( + Some: async credentialNonceEndpoint => { - token = await session.AuthorizationData.IssuerMetadata.CredentialNonceEndpoint.Match( - Some: async credentialNonceEndpoint => + var credentialNonce = await _credentialNonceService.GetCredentialNonce(credentialNonceEndpoint); + return token.Match>( + oAuth => oAuth with { CNonce = credentialNonce.Value }, + dPop => dPop with { - var credentialNonce = await _credentialNonceService.GetCredentialNonce(credentialNonceEndpoint); - return token.Match>( - oAuth => oAuth with { CNonce = credentialNonce.Value }, - dPop => dPop with - { - Token = dPop.Token with { CNonce = credentialNonce.Value } - }); - }, - None: () => - { - return Task.FromResult>( token.Match>( - oAuth => oAuth with { CNonce = cNonce.ToNullable() }, - dPop => dPop with - { - Token = dPop.Token with { CNonce = cNonce.ToNullable() } - })); + Token = dPop.Token with { CNonce = credentialNonce.Value } }); - - var record = sdJwt.Decoded.ToRecord( - configuration.AsT0, - response.KeyId, - credentialSet.CredentialSetId, - creds.Count > 1); - - await _sdJwtService.AddAsync(context, record); - - credentialSet.AddSdJwtData(record); }, - async mdoc => + None: () => { - token = await session.AuthorizationData.IssuerMetadata.CredentialNonceEndpoint.Match( - Some: async credentialNonceEndpoint => + return Task.FromResult>( token.Match>( + oAuth => oAuth with { CNonce = cNonce.ToNullable() }, + dPop => dPop with { - var credentialNonce = await _credentialNonceService.GetCredentialNonce(credentialNonceEndpoint); - return token.Match>( - oAuth => oAuth with { CNonce = credentialNonce.Value }, - dPop => dPop with - { - Token = dPop.Token with { CNonce = credentialNonce.Value } - }); - }, - None: () => - { - return Task.FromResult>( token.Match>( - oAuth => oAuth with { CNonce = cNonce.ToNullable() }, - dPop => dPop with - { - Token = dPop.Token with { CNonce = cNonce.ToNullable() } - })); - }); - - var displays = MdocFun.CreateMdocDisplays(configuration.AsT1); - - var record = mdoc.Decoded.ToRecord( - displays, - response.KeyId, - credentialSet.CredentialSetId, - creds.Count > 1); - - await _mdocStorage.Add(record); - - credentialSet.AddMDocData(record, session.AuthorizationData.IssuerMetadata.CredentialIssuer); - }); - } - }, - // ReSharper disable once UnusedParameter.Local - transactionId => throw new NotImplementedException()); - - await result.OnSuccess(task => task); + Token = dPop.Token with { CNonce = cNonce.ToNullable() } + })); + }); + + foreach (var credential in creds) + { + await credential.Value.Match( + async sdJwt => + { + var record = sdJwt.Decoded.ToRecord( + configuration.Value.AsT0, + response.KeyId, + credentialSet.CredentialSetId, + creds.Count > 1); + + await _sdJwtService.AddAsync(context, record); + + credentialSet.AddSdJwtData(record); + // credentialSets.Add(credentialSet); + }, + async mdoc => + { + var displays = MdocFun.CreateMdocDisplays(configuration.Value.AsT1); + + var record = mdoc.Decoded.ToRecord( + displays, + response.KeyId, + credentialSet.CredentialSetId, + creds.Count > 1); + + await _mdocStorage.Add(record); + + credentialSet.AddMDocData(record, session.AuthorizationData.IssuerMetadata.CredentialIssuer); + }); + } + credentialSets.Add(credentialSet); + }, + // ReSharper disable once UnusedParameter.Local + transactionId => throw new NotImplementedException()); + + await result.OnSuccess(async tasks => await Task.WhenAll(tasks)); } - await _credentialSetStorage.Add(credentialSet); + foreach (var credentialSet in credentialSets) + { + await _credentialSetStorage.Add(credentialSet); + } await _authFlowSessionStorage.DeleteAsync(context, session.AuthFlowSessionState); - return credentialSet; + return credentialSets; } //TODO: Refactor this C'' method into current flows (too much duplicate code) @@ -502,8 +495,7 @@ public async Task> RequestOnDemandCredentialSe .AuthorizationData .IssuerMetadata .CredentialConfigurationsSupported - .Where(config => session.AuthorizationData.CredentialConfigurationIds.Contains(config.Key)) - .Select(pair => pair.Value); + .Where(config => session.AuthorizationData.CredentialConfigurationIds.Contains(config.Key)); var scope = session .AuthorizationData @@ -528,119 +520,128 @@ public async Task> RequestOnDemandCredentialSe session.AuthorizationData.IssuerMetadata.CredentialNonceEndpoint); var credentialConfigs = credentialType.Match( - vct => credConfigurations.Where(config => config.Match( + vct => credConfigurations.Where(config => config.Value.Match( sdJwtConfiguration => sdJwtConfiguration.Vct == vct, _ => false)), - docType => credConfigurations.Where(config => config.Match( + docType => credConfigurations.Where(config => config.Value.Match( _ => false, mDocConfiguration => mDocConfiguration.DocType == docType))); List credentials = new(); - var credentialSetRecord = new CredentialSetRecord(); + var credentialSetRecords = new List(); //TODO: Make sure that it does not always request all available credConfigurations foreach (var configuration in credentialConfigs) { - var validResponse = await _credentialRequestService.RequestCredentials( + var validResponses = await _credentialRequestService.RequestCredentials( configuration, session.AuthorizationData.IssuerMetadata, token, session.AuthorizationData.ClientOptions, - authorizationRequest + authorizationRequest, + session.SpecVersion ); var result = - from response in validResponse - let cNonce = response.CNonce - let credentialsOrTransactionId = response.CredentialsOrTransactionId - select credentialsOrTransactionId.Match, TransactionId>>( - creds => - { - var records = new List(); - foreach (var credential in creds) + from responses in validResponses + let credentialSet = new CredentialSetRecord() + select + from response in responses + let cNonce = response.CNonce + let credentialsOrTransactionId = response.CredentialsOrTransactionId + select credentialsOrTransactionId.Match, TransactionId>>( + creds => { - var record = credential.Value.Match( - sdJwt => + var records = new List(); + foreach (var credential in creds) { - var record = sdJwt.Decoded.ToRecord( - configuration.AsT0, - response.KeyId, - credentialSetRecord.CredentialSetId, - creds.Count > 1); - - credentialSetRecord.AddSdJwtData(record); + var record = credential.Value.Match( + sdJwt => + { + var record = sdJwt.Decoded.ToRecord( + configuration.Value.AsT0, + response.KeyId, + credentialSet.CredentialSetId, + creds.Count > 1); - token = token.Match>( - oAuth => - { - session.AuthorizationData = session.AuthorizationData with + credentialSet.AddSdJwtData(record); + credentialSetRecords.Add(credentialSet); + + token = token.Match>( + oAuth => { - OAuthToken = oAuth - }; - return oAuth with { CNonce = cNonce.ToNullable() }; - }, - dPop => - { - session.AuthorizationData = session.AuthorizationData with + session.AuthorizationData = session.AuthorizationData with + { + OAuthToken = oAuth + }; + return oAuth with { CNonce = cNonce.ToNullable() }; + }, + dPop => { - OAuthToken = dPop.Token - }; - return dPop with { Token = dPop.Token with { CNonce = cNonce.ToNullable() } }; - }); - - return record; - }, - mdoc => - { - var displays = MdocFun.CreateMdocDisplays(configuration.AsT1); - - var record = mdoc.Decoded.ToRecord( - displays, - response.KeyId, - credentialSetRecord.CredentialSetId, - creds.Count > 1); + session.AuthorizationData = session.AuthorizationData with + { + OAuthToken = dPop.Token + }; + return dPop with { Token = dPop.Token with { CNonce = cNonce.ToNullable() } }; + }); - credentialSetRecord.AddMDocData(record, session.AuthorizationData.IssuerMetadata.CredentialIssuer); + return record; + }, + mdoc => + { + var displays = MdocFun.CreateMdocDisplays(configuration.Value.AsT1); + + var record = mdoc.Decoded.ToRecord( + displays, + response.KeyId, + credentialSet.CredentialSetId, + creds.Count > 1); - token = token.Match>( - oAuth => - { - session.AuthorizationData = session.AuthorizationData with + credentialSet.AddMDocData(record, session.AuthorizationData.IssuerMetadata.CredentialIssuer); + credentialSetRecords.Add(credentialSet); + + token = token.Match>( + oAuth => { - OAuthToken = oAuth - }; - return oAuth with { CNonce = cNonce.ToNullable() }; - }, - dPop => - { - session.AuthorizationData = session.AuthorizationData with + session.AuthorizationData = session.AuthorizationData with + { + OAuthToken = oAuth + }; + return oAuth with { CNonce = cNonce.ToNullable() }; + }, + dPop => { - OAuthToken = dPop.Token - }; - return dPop with { Token = dPop.Token with { CNonce = cNonce.ToNullable() } }; - }); + session.AuthorizationData = session.AuthorizationData with + { + OAuthToken = dPop.Token + }; + return dPop with { Token = dPop.Token with { CNonce = cNonce.ToNullable() } }; + }); - return record; - }); - - records.Add(record); - } + return record; + }); + + records.Add(record); + } - return records; - }, - // ReSharper disable once UnusedParameter.Local - transactionId => throw new NotImplementedException()); + return records; + }, + // ReSharper disable once UnusedParameter.Local + transactionId => throw new NotImplementedException()); - result.OnSuccess(task => + result.OnSuccess(tasks => { - credentials.AddRange((List)task.Value); + foreach (var task in tasks) + { + credentials.AddRange((List)task.Value); + } return Unit.Default; }); } await _authFlowSessionStorage.UpdateAsync(context, session); - return new OnDemandCredentialSet(credentialSetRecord, credentials); + return new OnDemandCredentialSet(credentialSetRecords.First(), credentials); } private static AuthorizationCodeParameters CreateAndStoreCodeChallenge() diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/AuthFlowSessionRecordTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/AuthFlowSessionRecordTests.cs index fc2b1c11..6d3e5ba6 100644 --- a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/AuthFlowSessionRecordTests.cs +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/AuthFlowSessionRecordTests.cs @@ -51,7 +51,7 @@ public void Can_Encode_To_Json() var authorizationCodeParameters = new AuthorizationCodeParameters("hello", "world"); var sessionId = AuthFlowSessionState.CreateAuthFlowSessionState(); - var record = new AuthFlowSessionRecord(authorizationData, authorizationCodeParameters, sessionId); + var record = new AuthFlowSessionRecord(authorizationData, authorizationCodeParameters, sessionId, 15); // Act var recordSut = JObject.FromObject(record); From c91adcbd8e49a7b56044631627017690635f562c Mon Sep 17 00:00:00 2001 From: Johannes Tuerk Date: Mon, 19 May 2025 11:50:16 +0200 Subject: [PATCH 3/6] fix unit test Signed-off-by: Johannes Tuerk --- .../Oid4Vci/AuthFlow/Samples/AuthFlowSamples.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/Samples/AuthFlowSamples.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/Samples/AuthFlowSamples.cs index 5bf04c7f..225c86ff 100644 --- a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/Samples/AuthFlowSamples.cs +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/Samples/AuthFlowSamples.cs @@ -37,6 +37,7 @@ public static class AuthFlowSamples ["Verifier"] = "world" }, ["RecordVersion"] = 1, - ["Id"] = "598e7661-95a8-4531-b707-3d256d3c1745" + ["Id"] = "598e7661-95a8-4531-b707-3d256d3c1745", + ["spec_version"] = "15" }; } From 76822d53b07ba247c05627160eff9cf3e554d0fe Mon Sep 17 00:00:00 2001 From: Johannes Tuerk Date: Tue, 20 May 2025 10:02:17 +0200 Subject: [PATCH 4/6] fix AuthReqeustUri for VciAuthentication without PAR Signed-off-by: Johannes Tuerk --- .../Oid4Vci/Implementations/Oid4VciClientService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs index b00be653..c3bb2853 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs @@ -149,7 +149,7 @@ from issState in code.IssuerState var authServerMetadata = await FetchAuthorizationServerMetadataAsync(issuerMetadata, offer.CredentialOffer); var authorizationRequestUri = authServerMetadata.PushedAuthorizationRequestEndpoint.IsNullOrEmpty() - ? new Uri(authServerMetadata.AuthorizationEndpoint + "?" + vciAuthorizationRequest.ToQueryString()) + ? new Uri(authServerMetadata.AuthorizationEndpoint + vciAuthorizationRequest.ToQueryString()) : await GetRequestUriUsingPushedAuthorizationRequest(authServerMetadata, vciAuthorizationRequest); var authorizationData = new AuthorizationData( @@ -211,7 +211,7 @@ public async Task InitiateAuthFlow(Uri uri, ClientOptions clientOptions, Op null); var authorizationRequestUri = authServerMetadata.PushedAuthorizationRequestEndpoint.IsNullOrEmpty() - ? new Uri(authServerMetadata.AuthorizationEndpoint + "?" + vciAuthorizationRequest.ToQueryString()) + ? new Uri(authServerMetadata.AuthorizationEndpoint + vciAuthorizationRequest.ToQueryString()) : await GetRequestUriUsingPushedAuthorizationRequest(authServerMetadata, vciAuthorizationRequest); //TODO: Select multiple configurationIds From 4b2cded95fa24964575fbfd44ca03244d221e760 Mon Sep 17 00:00:00 2001 From: Johannes Tuerk Date: Tue, 20 May 2025 17:49:48 +0200 Subject: [PATCH 5/6] fix OnDemand flow Signed-off-by: Johannes Tuerk --- .../Abstractions/IOid4VciClientService.cs | 6 +- .../Implementations/Oid4VciClientService.cs | 205 ++++++++++-------- 2 files changed, 113 insertions(+), 98 deletions(-) diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs index 335682d3..f530201e 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs @@ -30,9 +30,10 @@ public interface IOid4VciClientService /// The issuers uri /// The client options /// Optional language tag + /// Specifies whether Sd-Jwt or MDoc should be issued /// Optional language tag /// - Task InitiateAuthFlow(Uri uri, ClientOptions clientOptions, Option language, int specVersion); + Task InitiateAuthFlow(Uri uri, ClientOptions clientOptions, Option language, Option> credentialType, int specVersion); /// /// Requests a verifiable credential using the authorization code flow. @@ -48,11 +49,10 @@ public interface IOid4VciClientService /// /// Holds authorization session relevant information. /// The AuthorizationRequest that is associated witht the ad-hoc crednetial issuance - /// Specifies whether Sd-Jwt or MDoc should be issued /// /// A list of credentials. /// - Task> RequestOnDemandCredentialSet(IssuanceSession issuanceSession, AuthorizationRequest authorizationRequest, OneOf credentialType); + Task>> RequestOnDemandCredentialSet(IssuanceSession issuanceSession, AuthorizationRequest authorizationRequest); /// /// Processes a credential offer diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs index c3bb2853..b7b95430 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs @@ -169,7 +169,7 @@ await _authFlowSessionStorage.StoreAsync( return authorizationRequestUri; } - public async Task InitiateAuthFlow(Uri uri, ClientOptions clientOptions, Option language, int specVersion) + public async Task InitiateAuthFlow(Uri uri, ClientOptions clientOptions, Option language, Option> credentialType, int specVersion) { var locale = language.Match( some => some, @@ -186,26 +186,37 @@ public async Task InitiateAuthFlow(Uri uri, ClientOptions clientOptions, Op var sessionId = AuthFlowSessionState.CreateAuthFlowSessionState(); var authorizationCodeParameters = CreateAndStoreCodeChallenge(); - var scope = validIssuerMetadata.CredentialConfigurationsSupported.First().Value.Match( - sdJwtConfig => sdJwtConfig.CredentialConfiguration.Scope.OnSome(scope => scope.ToString()), - mdDocConfig => mdDocConfig.CredentialConfiguration.Scope.OnSome(scope => scope.ToString()) - ); + var relevantConfigurations = validIssuerMetadata.CredentialConfigurationsSupported + .Where(config => + { + return credentialType.Match( + type => config.Value.Match( + sdJwtConfig => type.IsT0 && sdJwtConfig.Vct == type.AsT0, + mDocConfig => type.IsT1 && mDocConfig.DocType == type.AsT1), + () => true); + }).ToList(); - var authorizationDetails = validIssuerMetadata.CredentialConfigurationsSupported.First().Value.Match( - sdJwtConfig => new AuthorizationDetails( - validIssuerMetadata.CredentialConfigurationsSupported.First().Key.ToString(), - validIssuerMetadata.AuthorizationServers.ToNullable()?.Select(id => id.ToString()).ToArray()), - mdDocConfig => new AuthorizationDetails( - validIssuerMetadata.CredentialConfigurationsSupported.First().Key.ToString(), - validIssuerMetadata.AuthorizationServers.ToNullable()?.Select(id => id.ToString()).ToArray()) - ); + var scopes = relevantConfigurations + .Select(config => config.Value.Match( + sdJwtConfig => sdJwtConfig.CredentialConfiguration.Scope.OnSome(scope => scope.ToString()), + mdDocConfig => mdDocConfig.CredentialConfiguration.Scope.OnSome(scope => scope.ToString()) + )) + .Where(option => option.IsSome) + .Select(option => option.Fallback(string.Empty)) + .Distinct(); + + var authorizationDetails = relevantConfigurations + .Select(config => new AuthorizationDetails( + config.Key.ToString(), + validIssuerMetadata.AuthorizationServers.ToNullable()?.Select(id => id.ToString()).ToArray() + )).ToArray(); var vciAuthorizationRequest = new VciAuthorizationRequest( sessionId, clientOptions, authorizationCodeParameters, - [authorizationDetails], - scope.ToNullable(), + authorizationDetails, + string.Join(" ", scopes), null, null, null); @@ -219,8 +230,10 @@ public async Task InitiateAuthFlow(Uri uri, ClientOptions clientOptions, Op clientOptions, validIssuerMetadata, authServerMetadata, - Option.None, - validIssuerMetadata.CredentialConfigurationsSupported.Keys.ToList()); + Option.None, + relevantConfigurations + .Select(config => config.Key) + .ToList()); var context = await _agentProvider.GetContextAsync(); await _authFlowSessionStorage.StoreAsync( @@ -363,18 +376,18 @@ public async Task>> RequestCredentia var session = await _authFlowSessionStorage.GetAsync(context, issuanceSession.AuthFlowSessionState); - var credConfiguration = session + var relevantConfigurations = session .AuthorizationData .IssuerMetadata .CredentialConfigurationsSupported .Where(config => session.AuthorizationData.CredentialConfigurationIds.Contains(config.Key)); - - var scope = session - .AuthorizationData - .IssuerMetadata - .CredentialConfigurationsSupported.First().Value.Match( - sdJwtConfig => sdJwtConfig.CredentialConfiguration.Scope.OnSome(scope => scope.ToString()), - mdDocConfig => mdDocConfig.CredentialConfiguration.Scope.OnSome(scope => scope.ToString())); + + var scopes = relevantConfigurations + .Select(config => config.Value.Match( + sdJwtConfig => sdJwtConfig.CredentialConfiguration.Scope.OnSome(scope => scope.ToString()), + mdDocConfig => mdDocConfig.CredentialConfiguration.Scope.OnSome(scope => scope.ToString()))) + .Where(scope => scope.IsSome) + .Select(option => option.Fallback(string.Empty)); var tokenRequest = new TokenRequest { @@ -382,7 +395,7 @@ public async Task>> RequestCredentia RedirectUri = session.AuthorizationData.ClientOptions.RedirectUri, CodeVerifier = session.AuthorizationCodeParameters.Verifier, Code = issuanceSession.Code, - Scope = scope.ToNullable(), + Scope = string.Join(" ", scopes), ClientId = session.AuthorizationData.ClientOptions.ClientId }; @@ -393,7 +406,7 @@ public async Task>> RequestCredentia //TODO: Make sure that it does not always request all available credConfigurations var credentialSets = new List(); - foreach (var configuration in credConfiguration) + foreach (var configuration in relevantConfigurations) { var validResponses = await _credentialRequestService.RequestCredentials( configuration, @@ -485,24 +498,24 @@ await credential.Value.Match( //TODO: Refactor this C'' method into current flows (too much duplicate code) /// - public async Task> RequestOnDemandCredentialSet(IssuanceSession issuanceSession, AuthorizationRequest authorizationRequest, OneOf credentialType) + public async Task>> RequestOnDemandCredentialSet(IssuanceSession issuanceSession, AuthorizationRequest authorizationRequest) { var context = await _agentProvider.GetContextAsync(); var session = await _authFlowSessionStorage.GetAsync(context, issuanceSession.AuthFlowSessionState); - var credConfigurations = session + var relevantConfigurations = session .AuthorizationData .IssuerMetadata .CredentialConfigurationsSupported .Where(config => session.AuthorizationData.CredentialConfigurationIds.Contains(config.Key)); - var scope = session - .AuthorizationData - .IssuerMetadata - .CredentialConfigurationsSupported.First().Value.Match( - sdJwtConfig => sdJwtConfig.CredentialConfiguration.Scope.OnSome(scope => scope.ToString()), - mdDocConfig => mdDocConfig.CredentialConfiguration.Scope.OnSome(scope => scope.ToString())); + var scopes = relevantConfigurations + .Select(config => config.Value.Match( + sdJwtConfig => sdJwtConfig.CredentialConfiguration.Scope.OnSome(scope => scope.ToString()), + mdDocConfig => mdDocConfig.CredentialConfiguration.Scope.OnSome(scope => scope.ToString()))) + .Where(scope => scope.IsSome) + .Select(option => option.Fallback(string.Empty)); var tokenRequest = new TokenRequest { @@ -510,7 +523,7 @@ public async Task> RequestOnDemandCredentialSe RedirectUri = session.AuthorizationData.ClientOptions.RedirectUri, CodeVerifier = session.AuthorizationCodeParameters.Verifier, Code = issuanceSession.Code, - Scope = scope.ToNullable(), + Scope = string.Join(" ", scopes), ClientId = session.AuthorizationData.ClientOptions.ClientId }; @@ -519,19 +532,10 @@ public async Task> RequestOnDemandCredentialSe session.AuthorizationData.AuthorizationServerMetadata, session.AuthorizationData.IssuerMetadata.CredentialNonceEndpoint); - var credentialConfigs = credentialType.Match( - vct => credConfigurations.Where(config => config.Value.Match( - sdJwtConfiguration => sdJwtConfiguration.Vct == vct, - _ => false)), - docType => credConfigurations.Where(config => config.Value.Match( - _ => false, - mDocConfiguration => mDocConfiguration.DocType == docType))); - - List credentials = new(); - var credentialSetRecords = new List(); + var credentials = new List<(CredentialSetRecord, List)>(); //TODO: Make sure that it does not always request all available credConfigurations - foreach (var configuration in credentialConfigs) + foreach (var configuration in relevantConfigurations) { var validResponses = await _credentialRequestService.RequestCredentials( configuration, @@ -549,32 +553,25 @@ from responses in validResponses from response in responses let cNonce = response.CNonce let credentialsOrTransactionId = response.CredentialsOrTransactionId - select credentialsOrTransactionId.Match, TransactionId>>( - creds => + select credentialsOrTransactionId.Match( + async creds => { - var records = new List(); - foreach (var credential in creds) - { - var record = credential.Value.Match( - sdJwt => + token = await session.AuthorizationData.IssuerMetadata.CredentialNonceEndpoint.Match( + Some: async credentialNonceEndpoint => { - var record = sdJwt.Decoded.ToRecord( - configuration.Value.AsT0, - response.KeyId, - credentialSet.CredentialSetId, - creds.Count > 1); - - credentialSet.AddSdJwtData(record); - credentialSetRecords.Add(credentialSet); - - token = token.Match>( + var credentialNonce = await _credentialNonceService.GetCredentialNonce(credentialNonceEndpoint); + return token.Match>( oAuth => { session.AuthorizationData = session.AuthorizationData with { OAuthToken = oAuth }; - return oAuth with { CNonce = cNonce.ToNullable() }; + + return oAuth with + { + CNonce = credentialNonce.Value + }; }, dPop => { @@ -582,31 +579,23 @@ select credentialsOrTransactionId.Match, TransactionId>> { OAuthToken = dPop.Token }; - return dPop with { Token = dPop.Token with { CNonce = cNonce.ToNullable() } }; + + return dPop with + { + Token = dPop.Token with { CNonce = credentialNonce.Value } + }; }); - - return record; }, - mdoc => + None: () => { - var displays = MdocFun.CreateMdocDisplays(configuration.Value.AsT1); - - var record = mdoc.Decoded.ToRecord( - displays, - response.KeyId, - credentialSet.CredentialSetId, - creds.Count > 1); - - credentialSet.AddMDocData(record, session.AuthorizationData.IssuerMetadata.CredentialIssuer); - credentialSetRecords.Add(credentialSet); - - token = token.Match>( + return Task.FromResult>( token.Match>( oAuth => { session.AuthorizationData = session.AuthorizationData with { OAuthToken = oAuth }; + return oAuth with { CNonce = cNonce.ToNullable() }; }, dPop => @@ -615,33 +604,59 @@ select credentialsOrTransactionId.Match, TransactionId>> { OAuthToken = dPop.Token }; - return dPop with { Token = dPop.Token with { CNonce = cNonce.ToNullable() } }; - }); + + return dPop with + { + Token = dPop.Token with { CNonce = cNonce.ToNullable() } + }; + })); + }); + + var records = new List(); + foreach (var credential in creds) + { + var record = credential.Value.Match( + sdJwt => + { + var record = sdJwt.Decoded.ToRecord( + configuration.Value.AsT0, + response.KeyId, + credentialSet.CredentialSetId, + creds.Count > 1); + + credentialSet.AddSdJwtData(record); + + return record; + }, + mdoc => + { + var displays = MdocFun.CreateMdocDisplays(configuration.Value.AsT1); + + var record = mdoc.Decoded.ToRecord( + displays, + response.KeyId, + credentialSet.CredentialSetId, + creds.Count > 1); + + credentialSet.AddMDocData(record, session.AuthorizationData.IssuerMetadata.CredentialIssuer); return record; }); records.Add(record); } - - return records; + + credentials.Add((credentialSet, records)); }, // ReSharper disable once UnusedParameter.Local transactionId => throw new NotImplementedException()); - result.OnSuccess(tasks => - { - foreach (var task in tasks) - { - credentials.AddRange((List)task.Value); - } - return Unit.Default; - }); + await result.OnSuccess(async tasks => await Task.WhenAll(tasks)); } await _authFlowSessionStorage.UpdateAsync(context, session); - - return new OnDemandCredentialSet(credentialSetRecords.First(), credentials); + + return credentials.Select(credential => new OnDemandCredentialSet(credential.Item1, credential.Item2)).ToList(); } private static AuthorizationCodeParameters CreateAndStoreCodeChallenge() From 4fb7ef5cb1b19f279eb9b6942f3dc139a55b5681 Mon Sep 17 00:00:00 2001 From: kenkosmowski Date: Mon, 2 Jun 2025 11:43:07 +0200 Subject: [PATCH 6/6] improve vci draft version handling Signed-off-by: kenkosmowski --- .../Abstractions/IOid4VciClientService.cs | 2 +- .../Abstractions/IAuthFlowSessionStorage.cs | 3 +- .../Implementations/AuthFlowSessionStorage.cs | 8 +-- .../AuthFlow/Records/AuthFlowSessionRecord.cs | 6 +- .../Errors/ScopeIsNullOrWhitespaceError.cs | 6 ++ .../Models/ScopedCredentialConfiguration.cs | 31 ++++++++++ .../Abstractions/ICredentialRequestService.cs | 2 +- .../CredentialRequestService.cs | 4 +- .../CredRequest/Models/CredentialRequest.cs | 57 ++++++++++--------- .../Implementations/Oid4VciClientService.cs | 13 +++-- 10 files changed, 86 insertions(+), 46 deletions(-) create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/ScopeIsNullOrWhitespaceError.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Models/ScopedCredentialConfiguration.cs diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs index f530201e..980c59ce 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs @@ -33,7 +33,7 @@ public interface IOid4VciClientService /// Specifies whether Sd-Jwt or MDoc should be issued /// Optional language tag /// - Task InitiateAuthFlow(Uri uri, ClientOptions clientOptions, Option language, Option> credentialType, int specVersion); + Task InitiateAuthFlow(Uri uri, ClientOptions clientOptions, Option language, Option> credentialType, Option specVersion); /// /// Requests a verifiable credential using the authorization code flow. diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Abstractions/IAuthFlowSessionStorage.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Abstractions/IAuthFlowSessionStorage.cs index 0f2e490d..be79f0db 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Abstractions/IAuthFlowSessionStorage.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Abstractions/IAuthFlowSessionStorage.cs @@ -1,4 +1,5 @@ using Hyperledger.Aries.Agents; +using LanguageExt; using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Records; @@ -51,5 +52,5 @@ Task StoreAsync( AuthorizationData authorizationData, AuthorizationCodeParameters authorizationCodeParameters, AuthFlowSessionState authFlowSessionState, - int specVersion = 15); + Option specVersion); } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Implementations/AuthFlowSessionStorage.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Implementations/AuthFlowSessionStorage.cs index 8e72ba8a..2317bfff 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Implementations/AuthFlowSessionStorage.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Implementations/AuthFlowSessionStorage.cs @@ -1,5 +1,6 @@ using Hyperledger.Aries.Agents; using Hyperledger.Aries.Storage; +using LanguageExt; using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Abstractions; using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Records; @@ -22,18 +23,17 @@ public AuthFlowSessionStorage(IWalletRecordService recordService) private readonly IWalletRecordService _recordService; /// - public async Task StoreAsync( - IAgentContext agentContext, + public async Task StoreAsync(IAgentContext agentContext, AuthorizationData authorizationData, AuthorizationCodeParameters authorizationCodeParameters, AuthFlowSessionState authFlowSessionState, - int specVersion) + Option specVersion) { var record = new AuthFlowSessionRecord( authorizationData, authorizationCodeParameters, authFlowSessionState, - specVersion); + specVersion.ToNullable()); await _recordService.AddAsync(agentContext.Wallet, record); diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Records/AuthFlowSessionRecord.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Records/AuthFlowSessionRecord.cs index 805dcbd4..576fffc2 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Records/AuthFlowSessionRecord.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Records/AuthFlowSessionRecord.cs @@ -41,7 +41,7 @@ public AuthFlowSessionState AuthFlowSessionState /// /// Used to track the VCI Specification verison /// - public int SpecVersion { get; } + public int? SpecVersion { get; } /// /// Initializes a new instance of the class. @@ -68,7 +68,7 @@ public AuthFlowSessionRecord( AuthorizationData authorizationData, AuthorizationCodeParameters authorizationCodeParameters, AuthFlowSessionState authFlowSessionState, - int specVersion) + int? specVersion) { AuthFlowSessionState = authFlowSessionState; RecordVersion = 1; @@ -130,7 +130,7 @@ public static AuthFlowSessionRecord DecodeFromJson(JObject json) var authorizationData = AuthorizationDataFun .DecodeFromJson(json[AuthorizationDataJsonKey]!.ToObject()!); - var specVersion = json[SpecVersionJsonKey]!.ToObject(); + var specVersion = json[SpecVersionJsonKey]!.ToObject(); var result = new AuthFlowSessionRecord(authorizationData, authCodeParameters!, id, specVersion); diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/ScopeIsNullOrWhitespaceError.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/ScopeIsNullOrWhitespaceError.cs new file mode 100644 index 00000000..54fdd53b --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/ScopeIsNullOrWhitespaceError.cs @@ -0,0 +1,6 @@ +using WalletFramework.Core.Functional; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Errors; + +public record ScopeIsNullOrWhitespaceError() + : Error("The scope is null or whitespace"); diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Models/ScopedCredentialConfiguration.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Models/ScopedCredentialConfiguration.cs new file mode 100644 index 00000000..e825e5ff --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Models/ScopedCredentialConfiguration.cs @@ -0,0 +1,31 @@ +using LanguageExt; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models; + +public record ScopedCredentialConfiguration(CredentialConfigurationId CredentialConfigurationId, Option Scope); + +public static class ScopedCredentialConfigurationExtensions +{ + private const string ScopeJsonKey = "scope"; + private const string CredentialConfigurationIdJsonKey = "credential_configuration_id"; + + public static JObject EncodeToJson(this ScopedCredentialConfiguration scopedCredentialConfiguration) + { + return new JObject + { + { ScopeJsonKey, scopedCredentialConfiguration.Scope.MatchUnsafe(scope => scope.ToString(), () => null) }, + { CredentialConfigurationIdJsonKey, scopedCredentialConfiguration.CredentialConfigurationId.ToString() } + }; + } + + public static ScopedCredentialConfiguration DecodeFromJson(JObject json) + { + var scope = Scope.OptionalScope(json[ScopeJsonKey]!); + var credentialConfigurationId = CredentialConfigurationId.ValidCredentialConfigurationId(json[CredentialConfigurationIdJsonKey]!.ToString()).UnwrapOrThrow(); + + return new ScopedCredentialConfiguration(credentialConfigurationId, scope); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Abstractions/ICredentialRequestService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Abstractions/ICredentialRequestService.cs index e9ff7e48..cc9ec950 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Abstractions/ICredentialRequestService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Abstractions/ICredentialRequestService.cs @@ -20,5 +20,5 @@ public Task>> RequestCredentials( OneOf token, Option clientOptions, Option authorizationRequest, - int specVersion = 15); + Option specVersion); } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs index edfdb646..5b3c5f3b 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs @@ -51,7 +51,7 @@ private async Task CreateCredentialRequest( Format format, OneOf credentialIdentification, OneOf token, - int specVersion, + Option specVersion, IssuerMetadata issuerMetadata, Option clientOptions, Option authorizationRequest) @@ -139,7 +139,7 @@ async Task>> ICredentialRequestServic OneOf token, Option clientOptions, Option authorizationRequest, - int specVersion) + Option specVersion) { var credentialIdentifications = token.Match( diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/CredentialRequest.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/CredentialRequest.cs index d4d4117e..8234754f 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/CredentialRequest.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/CredentialRequest.cs @@ -14,30 +14,13 @@ namespace WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Models; /// This request contains the format of the credential, the type of credential, /// and a proof of possession of the key material the issued credential shall be bound to. /// -public record CredentialRequest(OneOf CredentialIdentification, Format Format, int specVersion, Option Proof, Option Proofs, Option SessionTranscript) -{ - /// - /// Gets the proof of possession of the key material the issued credential shall be bound to. - /// - public Option Proof { get; } = Proof; - - /// - /// Gets one or more proof of possessions of the key material the issued credential shall be bound to. - /// - public Option Proofs { get; } = Proofs; - - //TODO: Remove when backward compatibility is not needed anymore - /// - /// Gets the format of the credential to be issued. - /// - public Format Format { get; } = Format; - - public Option SessionTranscript { get; } = SessionTranscript; - - public OneOf CredentialIdentification { get; } = CredentialIdentification; - - public int SpecVersion { get; } = specVersion; -} +public record CredentialRequest( + OneOf CredentialIdentification, + Format Format, + Option SpecVersion, + Option Proof, + Option Proofs, + Option SessionTranscript); public static class CredentialRequestFun { @@ -75,10 +58,28 @@ public static JObject EncodeToJson(this CredentialRequest request) }, configurationId => { - if (request.SpecVersion == 15) - result.Add(CredentialConfigurationIdKey, configurationId.ToString()); - else - result.Add(FormatJsonKey, request.Format.ToString()); + request.SpecVersion.Match(specVersion => + { + switch (specVersion) + { + case 14: + result.Add(FormatJsonKey, request.Format.ToString()); + break; + case 15: + result.Add(CredentialConfigurationIdKey, configurationId.ToString()); + break; + default: + result.Add(FormatJsonKey, request.Format.ToString()); + result.Add(CredentialConfigurationIdKey, configurationId.ToString()); + break; + } + }, + () => + { + result.Add(FormatJsonKey, request.Format.ToString()); + result.Add(CredentialConfigurationIdKey, configurationId.ToString()); + } + ); return Unit.Default; }); diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs index b7b95430..02c41a84 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs @@ -164,12 +164,13 @@ await _authFlowSessionStorage.StoreAsync( context, authorizationData, authorizationCodeParameters, - sessionId); + sessionId, + Option.None); return authorizationRequestUri; } - public async Task InitiateAuthFlow(Uri uri, ClientOptions clientOptions, Option language, Option> credentialType, int specVersion) + public async Task InitiateAuthFlow(Uri uri, ClientOptions clientOptions, Option language, Option> credentialType, Option specVersion) { var locale = language.Match( some => some, @@ -297,7 +298,8 @@ from preAuthCode in grants.PreAuthorizedCode issuerMetadata, token, Option.None, - Option.None); + Option.None, + Option.None); var credentialSets = new List(); var result = @@ -414,7 +416,7 @@ public async Task>> RequestCredentia token, session.AuthorizationData.ClientOptions, Option.None, - session.SpecVersion); + session.SpecVersion.ToOption()); var result = from responses in validResponses @@ -543,8 +545,7 @@ public async Task>> RequestOnDeman token, session.AuthorizationData.ClientOptions, authorizationRequest, - session.SpecVersion - ); + session.SpecVersion.ToOption()); var result = from responses in validResponses