diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs index 5098f2b9..980c59ce 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs @@ -30,8 +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); + Task InitiateAuthFlow(Uri uri, ClientOptions clientOptions, Option language, Option> credentialType, Option specVersion); /// /// Requests a verifiable credential using the authorization code flow. @@ -40,18 +42,17 @@ 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''. /// /// 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 @@ -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..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; @@ -44,10 +45,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, + Option specVersion); } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Implementations/AuthFlowSessionStorage.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Implementations/AuthFlowSessionStorage.cs index 0f0862e4..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,16 +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) + AuthFlowSessionState authFlowSessionState, + Option specVersion) { var record = new AuthFlowSessionRecord( authorizationData, authorizationCodeParameters, - authFlowSessionState); + authFlowSessionState, + specVersion.ToNullable()); 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..576fffc2 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/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 b159396c..cc9ec950 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, + Option specVersion); } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs index cdd912d4..5b3c5f3b 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, + Option 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, + Option 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..8234754f 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,25 +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(Format Format, 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; - - /// - /// Gets the format of the credential to be issued. - /// - public Format Format { get; } = Format; - - public Option SessionTranscript { get; } = SessionTranscript; -} +public record CredentialRequest( + OneOf CredentialIdentification, + Format Format, + Option SpecVersion, + Option Proof, + Option Proofs, + Option SessionTranscript); public static class CredentialRequestFun { @@ -37,6 +28,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 +50,40 @@ 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 => + { + 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; + }); + return result; } } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs index cd0235ff..02c41a84 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( @@ -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) + public async Task InitiateAuthFlow(Uri uri, ClientOptions clientOptions, Option language, Option> credentialType, Option specVersion) { var locale = language.Match( some => some, @@ -186,32 +187,43 @@ 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); 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 @@ -219,15 +231,18 @@ 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( context, authorizationData, authorizationCodeParameters, - sessionId); + sessionId, + specVersion); return authorizationRequestUri; }, @@ -251,12 +266,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 +289,72 @@ 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(); - + Option.None, + Option.None); + + 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,25 +372,24 @@ from metadata in _issuerMetadataService.ProcessMetadata(offer.CredentialIssuer, } /// - public async Task> RequestCredentialSet(IssuanceSession issuanceSession) + public async Task>> RequestCredentialSet(IssuanceSession issuanceSession) { var context = await _agentProvider.GetContextAsync(); 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)) - .Select(pair => pair.Value); - - 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())); + .Where(config => session.AuthorizationData.CredentialConfigurationIds.Contains(config.Key)); + + 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 { @@ -376,7 +397,7 @@ public async Task> RequestCredentialSet(Issuance RedirectUri = session.AuthorizationData.ClientOptions.RedirectUri, CodeVerifier = session.AuthorizationCodeParameters.Verifier, Code = issuanceSession.Code, - Scope = scope.ToNullable(), + Scope = string.Join(" ", scopes), ClientId = session.AuthorizationData.ClientOptions.ClientId }; @@ -385,132 +406,118 @@ 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 - foreach (var configuration in credConfiguration) + var credentialSets = new List(); + foreach (var configuration in relevantConfigurations) { - var validResponse = await _credentialRequestService.RequestCredentials( + var validResponses = await _credentialRequestService.RequestCredentials( configuration, session.AuthorizationData.IssuerMetadata, token, session.AuthorizationData.ClientOptions, - Option.None); + Option.None, + session.SpecVersion.ToOption()); 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 => - { - 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 { - 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) /// - 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)) - .Select(pair => pair.Value); + .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 { @@ -518,7 +525,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 }; @@ -527,120 +534,130 @@ public async Task> RequestOnDemandCredentialSe session.AuthorizationData.AuthorizationServerMetadata, session.AuthorizationData.IssuerMetadata.CredentialNonceEndpoint); - var credentialConfigs = credentialType.Match( - vct => credConfigurations.Where(config => config.Match( - sdJwtConfiguration => sdJwtConfiguration.Vct == vct, - _ => false)), - docType => credConfigurations.Where(config => config.Match( - _ => false, - mDocConfiguration => mDocConfiguration.DocType == docType))); - - List credentials = new(); - var credentialSetRecord = new CredentialSetRecord(); + 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 validResponse = await _credentialRequestService.RequestCredentials( + var validResponses = await _credentialRequestService.RequestCredentials( configuration, session.AuthorizationData.IssuerMetadata, token, session.AuthorizationData.ClientOptions, - authorizationRequest - ); + authorizationRequest, + session.SpecVersion.ToOption()); 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( + async creds => { - var record = credential.Value.Match( - sdJwt => - { - var record = sdJwt.Decoded.ToRecord( - configuration.AsT0, - response.KeyId, - credentialSetRecord.CredentialSetId, - creds.Count > 1); - - credentialSetRecord.AddSdJwtData(record); - - token = token.Match>( - oAuth => - { - session.AuthorizationData = session.AuthorizationData with + token = await session.AuthorizationData.IssuerMetadata.CredentialNonceEndpoint.Match( + Some: async credentialNonceEndpoint => + { + var credentialNonce = await _credentialNonceService.GetCredentialNonce(credentialNonceEndpoint); + return 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 = credentialNonce.Value + }; + }, + dPop => + { + session.AuthorizationData = session.AuthorizationData with + { + OAuthToken = dPop.Token + }; + + return dPop with + { + Token = dPop.Token with { CNonce = credentialNonce.Value } + }; + }); + }, + None: () => + { + return Task.FromResult>( token.Match>( + oAuth => { - OAuthToken = dPop.Token - }; - return dPop with { Token = dPop.Token with { CNonce = cNonce.ToNullable() } }; - }); - - return record; - }, - mdoc => + session.AuthorizationData = session.AuthorizationData with + { + OAuthToken = oAuth + }; + + return oAuth with { CNonce = cNonce.ToNullable() }; + }, + dPop => + { + session.AuthorizationData = session.AuthorizationData with + { + OAuthToken = dPop.Token + }; + + return dPop with + { + Token = dPop.Token with { CNonce = cNonce.ToNullable() } + }; + })); + }); + + var records = new List(); + foreach (var credential in creds) { - var displays = MdocFun.CreateMdocDisplays(configuration.AsT1); - - var record = mdoc.Decoded.ToRecord( - displays, - response.KeyId, - credentialSetRecord.CredentialSetId, - creds.Count > 1); + var record = credential.Value.Match( + sdJwt => + { + var record = sdJwt.Decoded.ToRecord( + configuration.Value.AsT0, + response.KeyId, + credentialSet.CredentialSetId, + creds.Count > 1); - credentialSetRecord.AddMDocData(record, session.AuthorizationData.IssuerMetadata.CredentialIssuer); + credentialSet.AddSdJwtData(record); - token = token.Match>( - oAuth => - { - session.AuthorizationData = session.AuthorizationData with - { - OAuthToken = oAuth - }; - return oAuth with { CNonce = cNonce.ToNullable() }; - }, - dPop => - { - session.AuthorizationData = session.AuthorizationData with - { - OAuthToken = dPop.Token - }; - return dPop with { Token = dPop.Token with { CNonce = cNonce.ToNullable() } }; - }); + return record; + }, + mdoc => + { + var displays = MdocFun.CreateMdocDisplays(configuration.Value.AsT1); + + var record = mdoc.Decoded.ToRecord( + displays, + response.KeyId, + credentialSet.CredentialSetId, + creds.Count > 1); - return record; - }); - - records.Add(record); - } + credentialSet.AddMDocData(record, session.AuthorizationData.IssuerMetadata.CredentialIssuer); - return records; - }, - // ReSharper disable once UnusedParameter.Local - transactionId => throw new NotImplementedException()); + return record; + }); + + records.Add(record); + } + + credentials.Add((credentialSet, records)); + }, + // ReSharper disable once UnusedParameter.Local + transactionId => throw new NotImplementedException()); - result.OnSuccess(task => - { - 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(credentialSetRecord, credentials); + + return credentials.Select(credential => new OnDemandCredentialSet(credential.Item1, credential.Item2)).ToList(); } 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); 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" }; }