From 41a16eefd1146019f6c612238bf3a7b811379721 Mon Sep 17 00:00:00 2001 From: Johannes Tuerk Date: Tue, 30 Jul 2024 15:11:49 +0200 Subject: [PATCH 1/3] add issuer initiated auth flow Signed-off-by: Johannes Tuerk --- .../Abstractions/IOid4VciClientService.cs | 11 ++- .../DPop/Implementations/DPopHttpClient.cs | 4 + .../Implementations/Oid4VciClientService.cs | 73 +++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs index 8865cad2..491df4ab 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs @@ -15,12 +15,21 @@ namespace WalletFramework.Oid4Vc.Oid4Vci.Abstractions; public interface IOid4VciClientService { /// - /// Initiates the authorization process of the VCI authorization code flow. + /// Initiates the issuer initiated authorization process of the VCI authorization code flow. /// /// The offer metadata /// The client options /// Task InitiateAuthFlow(CredentialOfferMetadata offer, ClientOptions clientOptions); + + /// + /// Initiates the wallet initiate authorization process of the VCI authorization code flow. + /// + /// The issuers uri + /// The client options + /// Optional language tag + /// + Task InitiateAuthFlow(Uri uri, ClientOptions clientOptions, Option language); /// /// Requests a verifiable credential using the authorization code flow. diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Implementations/DPopHttpClient.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Implementations/DPopHttpClient.cs index 25b2f4d8..4bb579cc 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Implementations/DPopHttpClient.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Implementations/DPopHttpClient.cs @@ -71,6 +71,10 @@ public async Task Post( httpClient.WithDPopHeader(newDpop); response = await httpClient.PostAsync(requestUri, getContent()); + + config = response.Headers.TryGetValues("DPoP-Nonce", out var refreshedDpopNonce) + ? config with { Nonce = new DPopNonce(refreshedDpopNonce?.First()!)} + : config; } await ThrowIfInvalidGrantError(response); diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs index ac2969cd..d88fcda7 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs @@ -167,6 +167,71 @@ await _authFlowSessionStorage.StoreAsync( return authorizationRequestUri; } + public async Task InitiateAuthFlow(Uri uri, ClientOptions clientOptions, Option language) + { + var locale = language.Match( + some => some, + () => Constants.DefaultLocale); + + var issuerMetadata = _issuerMetadataService.ProcessMetadata(uri, locale); + + return await issuerMetadata.Match( + async validIssuerMetadata => + { + 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 par = new PushedAuthorizationRequest( + sessionId, + clientOptions, + authorizationCodeParameters, + null, + scope.ToNullable(), + null, + null, + null); + + var authServerMetadata = + await FetchAuthorizationServerMetadataAsync(validIssuerMetadata); + + _httpClient.DefaultRequestHeaders.Clear(); + var response = await _httpClient.PostAsync( + authServerMetadata.PushedAuthorizationRequestEndpoint, + par.ToFormUrlEncoded() + ); + + var parResponse = DeserializeObject(await response.Content.ReadAsStringAsync()) + ?? throw new InvalidOperationException("Failed to deserialize the PAR response."); + + var authorizationRequestUri = new Uri(authServerMetadata.AuthorizationEndpoint + + "?client_id=" + par.ClientId + + "&request_uri=" + System.Net.WebUtility.UrlEncode(parResponse.RequestUri.ToString())); + + //TODO: Select multiple configurationIds + var authorizationData = new AuthorizationData( + clientOptions, + validIssuerMetadata, + authServerMetadata, + new List(){validIssuerMetadata.CredentialConfigurationsSupported.Keys.First()}); + + var context = await _agentProvider.GetContextAsync(); + await _authFlowSessionStorage.StoreAsync( + context, + authorizationData, + authorizationCodeParameters, + sessionId); + + return authorizationRequestUri; + }, + _ => throw new Exception("Fetching Issuer metadata failed") + ); + } + public async Task>> AcceptOffer(CredentialOfferMetadata credentialOfferMetadata, string? transactionCode) { var issuerMetadata = credentialOfferMetadata.IssuerMetadata; @@ -251,12 +316,20 @@ public async Task>> RequestCredential( .Select(pair => pair.Value) .First(); + 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 tokenRequest = new TokenRequest { GrantType = AuthorizationCodeGrantTypeIdentifier, RedirectUri = session.AuthorizationData.ClientOptions.RedirectUri, CodeVerifier = session.AuthorizationCodeParameters.Verifier, Code = issuanceSession.Code, + Scope = scope.ToNullable(), ClientId = session.AuthorizationData.ClientOptions.ClientId }; From 986cef85e397723edc3c9f0b131a0500945e3505 Mon Sep 17 00:00:00 2001 From: Johannes Tuerk Date: Tue, 30 Jul 2024 15:12:52 +0200 Subject: [PATCH 2/3] add workarounds for mdoc to allow malformed strucure for now Signed-off-by: Johannes Tuerk --- src/WalletFramework.MdocLib/IssuerSigned.cs | 20 +++++--- src/WalletFramework.MdocLib/Mdoc.cs | 47 ++++++++++++++++++- src/WalletFramework.MdocVc/MdocRecord.cs | 4 +- .../Oid4Vci/CredResponse/Mdoc/EncodedMdoc.cs | 2 +- 4 files changed, 62 insertions(+), 11 deletions(-) diff --git a/src/WalletFramework.MdocLib/IssuerSigned.cs b/src/WalletFramework.MdocLib/IssuerSigned.cs index 0157f184..f5ab1327 100644 --- a/src/WalletFramework.MdocLib/IssuerSigned.cs +++ b/src/WalletFramework.MdocLib/IssuerSigned.cs @@ -22,12 +22,10 @@ private IssuerSigned(NameSpaces nameSpaces, IssuerAuth issuerAuth) private static IssuerSigned Create(NameSpaces nameSpaces, IssuerAuth issuerAuth) => new(nameSpaces, issuerAuth); - internal static Validation ValidIssuerSigned(CBORObject mdoc) => - mdoc.GetByLabel(IssuerSignedLabel).OnSuccess(issuerSigned => - Valid(Create) - .Apply(ValidNameSpaces(issuerSigned)) - .Apply(ValidIssuerAuth(issuerSigned)) - ); + public static Validation ValidIssuerSigned(CBORObject issuerSigned) => + Valid(Create) + .Apply(ValidNameSpaces(issuerSigned)) + .Apply(ValidIssuerAuth(issuerSigned)); public CBORObject Encode() { @@ -39,3 +37,13 @@ public CBORObject Encode() return cbor; } } + +public static class IssuerSignedFun +{ + // TODO: This is only a hack currently, the doctype of the mdoc and the mso must be validated normally + public static Mdoc ToMdoc(this IssuerSigned issuerSigned) + { + var docType = issuerSigned.IssuerAuth.Payload.DocType; + return new Mdoc(docType, issuerSigned); + } +} diff --git a/src/WalletFramework.MdocLib/Mdoc.cs b/src/WalletFramework.MdocLib/Mdoc.cs index 767e2dcb..e9a4ce36 100644 --- a/src/WalletFramework.MdocLib/Mdoc.cs +++ b/src/WalletFramework.MdocLib/Mdoc.cs @@ -20,7 +20,7 @@ public record Mdoc // TODO: mdoc authentication // public DeviceSigned DeviceSigned { get; } - private Mdoc(DocType docType, IssuerSigned issuerSigned) + public Mdoc(DocType docType, IssuerSigned issuerSigned) { DocType = docType; IssuerSigned = issuerSigned; @@ -68,12 +68,55 @@ public static Validation ValidMdoc(string base64UrlencodedCborByteString) return from bytes in decodeBase64Url(base64UrlencodedCborByteString) from cbor in parseCborByteString(bytes) + from issuerSigned in cbor.GetByLabel(IssuerSignedLabel) from mdoc in Valid(Create) .Apply(ValidDoctype(cbor)) - .Apply(ValidIssuerSigned(cbor)) + .Apply(ValidIssuerSigned(issuerSigned)) from validMdoc in validateIntegrity(mdoc) select validMdoc; } + + //TODO: Workaround because PId Issuer only implemented issuer signed, Delete this overload when PID Issuer is fixed!! + public static Validation FromIssuerSigned(string base64UrlencodedCborByteString) + { + var decodeBase64Url = new Func>(str => + { + try + { + return Base64UrlEncoder.DecodeBytes(str)!; + } + catch (Exception e) + { + return new InvalidBase64UrlEncodingError(e); + } + }); + + var parseCborByteString = new Func>(bytes => + { + try + { + return CBORObject.DecodeFromBytes(bytes); + } + catch (Exception e) + { + return new InvalidCborByteStringError("mdocResponse", e); + } + }); + + var validateIntegrity = new List> + { + MdocFun.DocTypeMatches, + MdocFun.DigestsMatch + } + .AggregateValidators(); + + return + from bytes in decodeBase64Url(base64UrlencodedCborByteString) + from cbor in parseCborByteString(bytes) + from issuerSigned in ValidIssuerSigned(cbor) + from validMdoc in validateIntegrity(issuerSigned.ToMdoc()) + select validMdoc; + } public record InvalidBase64UrlEncodingError(Exception E) : Error("String is not Base64UrlEncoded", E); } diff --git a/src/WalletFramework.MdocVc/MdocRecord.cs b/src/WalletFramework.MdocVc/MdocRecord.cs index 6fa74d57..56582ed0 100644 --- a/src/WalletFramework.MdocVc/MdocRecord.cs +++ b/src/WalletFramework.MdocVc/MdocRecord.cs @@ -75,12 +75,12 @@ public static class MdocRecordFun public static MdocRecord DecodeFromJson(JObject json) { var id = json[nameof(RecordBase.Id)]!.ToString(); - + var mdocStr = json[MdocJsonKey]!.ToString(); var mdoc = Mdoc .ValidMdoc(mdocStr) .UnwrapOrThrow(new InvalidOperationException($"The MdocRecord with ID: {id} is corrupt")); - + var displays = from jToken in json.GetByKey(MdocDisplaysJsonKey).ToOption() from jArray in jToken.ToJArray().ToOption() diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredResponse/Mdoc/EncodedMdoc.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredResponse/Mdoc/EncodedMdoc.cs index 4b6daf84..acd2c765 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredResponse/Mdoc/EncodedMdoc.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredResponse/Mdoc/EncodedMdoc.cs @@ -25,7 +25,7 @@ public static Validation ValidEncodedMdoc(JValue mdoc) var str = mdoc.ToString(CultureInfo.InvariantCulture); return MdocLib.Mdoc - .ValidMdoc(str) + .FromIssuerSigned(str) .OnSuccess(mdoc1 => new EncodedMdoc(str, mdoc1)); } } From d9aa634eb5288f762da7a22ea89c3c109d144950 Mon Sep 17 00:00:00 2001 From: Johannes Tuerk Date: Wed, 31 Jul 2024 13:31:22 +0200 Subject: [PATCH 3/3] support sdJwt and mDoc Cred Request at once Signed-off-by: Johannes Tuerk --- .../Abstractions/IOid4VciClientService.cs | 2 +- .../Authorization/DPop/Models/DPopToken.cs | 2 +- .../Authorization/Models/OAuthToken.cs | 2 +- .../Implementations/Oid4VciClientService.cs | 82 +++++++++++-------- .../ISdJwtVcHolderService.cs | 12 ++- .../SdJwtVcHolderService.cs | 16 ++-- .../Oid4Vp/Oid4VpClientServiceTests.cs | 2 +- 7 files changed, 69 insertions(+), 49 deletions(-) diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs index 491df4ab..456c97a3 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs @@ -38,7 +38,7 @@ public interface IOid4VciClientService /// /// A list of credentials. /// - Task>> RequestCredential(IssuanceSession issuanceSession); + Task>>> RequestCredential(IssuanceSession issuanceSession); /// /// Processes a credential offer diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Models/DPopToken.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Models/DPopToken.cs index bbb45b43..a4e29769 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Models/DPopToken.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Models/DPopToken.cs @@ -4,7 +4,7 @@ namespace WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Models; public record DPopToken { - internal OAuthToken Token { get; } + internal OAuthToken Token { get; init; } internal DPop DPop { get; } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/OAuthToken.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/OAuthToken.cs index f8b86ae2..b434ca81 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/OAuthToken.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/OAuthToken.cs @@ -7,7 +7,7 @@ namespace WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models; /// Represents a successful response from the OAuth 2.0 Authorization Server containing /// the issued access token and related information. /// -public class OAuthToken +public record OAuthToken { /// /// Indicates if the Token Request is still pending as the Credential Issuer diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs index d88fcda7..b3f7c9af 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs @@ -17,6 +17,7 @@ using WalletFramework.Core.Functional; using WalletFramework.Core.Localization; using WalletFramework.MdocVc; +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Models; using WalletFramework.SdJwtVc.Models.Records; using WalletFramework.SdJwtVc.Services.SdJwtVcHolderService; using static Newtonsoft.Json.JsonConvert; @@ -217,7 +218,7 @@ public async Task InitiateAuthFlow(Uri uri, ClientOptions clientOptions, Op clientOptions, validIssuerMetadata, authServerMetadata, - new List(){validIssuerMetadata.CredentialConfigurationsSupported.Keys.First()}); + validIssuerMetadata.CredentialConfigurationsSupported.Keys.ToList()); var context = await _agentProvider.GetContextAsync(); await _authFlowSessionStorage.StoreAsync( @@ -271,7 +272,7 @@ select credentialOrTransactionId.Match( { var record = sdJwt.Decoded.ToRecord(configuration.AsT0, issuerMetadata, response.KeyId); var context = await _agentProvider.GetContextAsync(); - await _sdJwtService.SaveAsync(context, record); + await _sdJwtService.AddAsync(context, record); return record; }, async mdoc => @@ -302,7 +303,7 @@ from metadata in _issuerMetadataService.ProcessMetadata(offer.CredentialIssuer, } /// - public async Task>> RequestCredential(IssuanceSession issuanceSession) + public async Task>>> RequestCredential(IssuanceSession issuanceSession) { var context = await _agentProvider.GetContextAsync(); @@ -313,8 +314,7 @@ public async Task>> RequestCredential( .IssuerMetadata .CredentialConfigurationsSupported .Where(config => session.AuthorizationData.CredentialConfigurationIds.Contains(config.Key)) - .Select(pair => pair.Value) - .First(); + .Select(pair => pair.Value); var scope = session .AuthorizationData @@ -336,37 +336,53 @@ public async Task>> RequestCredential( var token = await _tokenService.RequestToken( tokenRequest, session.AuthorizationData.AuthorizationServerMetadata); - - var validResponse = await _credentialRequestService.RequestCredentials( - credConfiguration, - session.AuthorizationData.IssuerMetadata, - token, - session.AuthorizationData.ClientOptions); + + List> credentials = new(); + //TODO: Make sure that it does not always request all available credConfigurations + foreach (var configuration in credConfiguration) + { + var validResponse = await _credentialRequestService.RequestCredentials( + configuration, + session.AuthorizationData.IssuerMetadata, + token, + session.AuthorizationData.ClientOptions); + + var result = + from response in validResponse + let cNonce = response.CNonce + let credentialOrTransactionId = response.CredentialOrTransactionId + select credentialOrTransactionId.Match( + async credential => await credential.Value.Match>>( + async sdJwt => + { + token = token.Match>( + oAuth => oAuth with { CNonce = cNonce.ToNullable()}, + dPop => dPop with { Token = dPop.Token with {CNonce = cNonce.ToNullable()}}); + + var record = sdJwt.Decoded.ToRecord(configuration.AsT0, session.AuthorizationData.IssuerMetadata, response.KeyId); + await _sdJwtService.AddAsync(context, record); + return record; + }, + async mdoc => + { + token = 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); + await _mdocStorage.Add(record); + return record; + }), + // ReSharper disable once UnusedParameter.Local + transactionId => throw new NotImplementedException()); + + await result.OnSuccess(async task => credentials.Add(await task)); + } await _authFlowSessionStorage.DeleteAsync(context, session.AuthFlowSessionState); - var result = - from response in validResponse - let credentialOrTransactionId = response.CredentialOrTransactionId - select credentialOrTransactionId.Match( - async credential => await credential.Value.Match>>( - async sdJwt => - { - var record = sdJwt.Decoded.ToRecord(credConfiguration.AsT0, session.AuthorizationData.IssuerMetadata, response.KeyId); - await _sdJwtService.SaveAsync(context, record); - return record; - }, - async mdoc => - { - var displays = MdocFun.CreateMdocDisplays(credConfiguration.AsT1); - var record = mdoc.Decoded.ToRecord(displays); - await _mdocStorage.Add(record); - return record; - }), - // ReSharper disable once UnusedParameter.Local - transactionId => throw new NotImplementedException()); - - return await result.OnSuccess(task => task); + return credentials; } private static AuthorizationCodeParameters CreateAndStoreCodeChallenge() diff --git a/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/ISdJwtVcHolderService.cs b/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/ISdJwtVcHolderService.cs index 72e1ec9e..174778ce 100644 --- a/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/ISdJwtVcHolderService.cs +++ b/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/ISdJwtVcHolderService.cs @@ -66,10 +66,18 @@ Task> ListAsync( int skip = 0); /// - /// Stores or updates a SD-JWT record. + /// Updates a SD-JWT record. /// /// The agent context. /// The SD-JWT record to be saved /// A task representing the asynchronous operation. The task result contains the ID of the stored JWT record. - Task SaveAsync(IAgentContext context, SdJwtRecord record); + Task UpdateAsync(IAgentContext context, SdJwtRecord record); + + /// + /// Adds a SD-JWT record. + /// + /// The agent context. + /// The SD-JWT record to be saved + /// A task representing the asynchronous operation. The task result contains the ID of the stored JWT record. + Task AddAsync(IAgentContext context, SdJwtRecord record); } diff --git a/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/SdJwtVcHolderService.cs b/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/SdJwtVcHolderService.cs index 4b1e91cd..fa5cd2f0 100644 --- a/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/SdJwtVcHolderService.cs +++ b/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/SdJwtVcHolderService.cs @@ -91,17 +91,13 @@ public Task> ListAsync( int skip = 0) => _recordService.SearchAsync(context.Wallet, query, null, count, skip); /// - public virtual async Task SaveAsync(IAgentContext context, SdJwtRecord record) - { - try - { + public virtual async Task AddAsync(IAgentContext context, SdJwtRecord record) => await _recordService.AddAsync(context.Wallet, record); - } - catch (WalletItemAlreadyExistsException) - { - await _recordService.UpdateAsync(context.Wallet, record); - } - } + + /// + public virtual async Task UpdateAsync(IAgentContext context, SdJwtRecord record) => + await _recordService.UpdateAsync(context.Wallet, record); + } internal static class SdJwtRecordExtensions diff --git a/test/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/Oid4Vp/Oid4VpClientServiceTests.cs b/test/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/Oid4Vp/Oid4VpClientServiceTests.cs index cc1cc3e5..e7b332dd 100644 --- a/test/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/Oid4Vp/Oid4VpClientServiceTests.cs +++ b/test/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/Oid4Vp/Oid4VpClientServiceTests.cs @@ -83,7 +83,7 @@ public async Task CanExecuteOpenId4VpFlow() var sdJwt = new SdJwtRecord(); - await _sdJwtVcHolderService.SaveAsync(_agent1!.Context, sdJwt); + await _sdJwtVcHolderService.AddAsync(_agent1!.Context, sdJwt); //Act var (authorizationRequest, credentials) =