From 05a671ece8edeac2be982f7b67b5acf228e77943 Mon Sep 17 00:00:00 2001 From: Johannes Tuerk <72355192+JoTiTu@users.noreply.github.com> Date: Thu, 21 Nov 2024 17:57:24 +0100 Subject: [PATCH] Improve auth code flow (#224) * fix AuthServer retrieval Signed-off-by: Johannes Tuerk * fix credential state updating Signed-off-by: Johannes Tuerk * fix batch issuance count Signed-off-by: Johannes Tuerk * allow auth code without PAR Signed-off-by: Johannes Tuerk * small improvements Signed-off-by: kenkosmowski --------- Signed-off-by: Johannes Tuerk Signed-off-by: kenkosmowski Co-authored-by: kenkosmowski --- .../CredentialSet/CredentialSetService.cs | 13 +- .../AuthFlow/Models/AuthorizationDetails.cs | 15 +- .../Models/PushedAuthorizationRequest.cs | 106 ------------ .../Models/VciAuthorizationRequest.cs | 150 ++++++++++++++++ .../Models/AuthorizationServerMetadata.cs | 10 ++ .../Models/CredentialDisplay.cs | 19 --- .../Models/CredentialLogo.cs | 44 ++--- .../Models/SdJwt/SdJwtConfiguration.cs | 22 +++ .../CredentialRequestService.cs | 2 +- .../Oid4Vci/Implementations/MdocFun.cs | 5 +- .../Implementations/Oid4VciClientService.cs | 161 +++++++++++------- .../Implementations/SdJwtRecordExtensions.cs | 20 +-- .../Services/VctMetadataService.cs | 2 +- 13 files changed, 312 insertions(+), 257 deletions(-) delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/PushedAuthorizationRequest.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/VciAuthorizationRequest.cs diff --git a/src/WalletFramework.Oid4Vc/CredentialSet/CredentialSetService.cs b/src/WalletFramework.Oid4Vc/CredentialSet/CredentialSetService.cs index 23615bf5..feaf3094 100644 --- a/src/WalletFramework.Oid4Vc/CredentialSet/CredentialSetService.cs +++ b/src/WalletFramework.Oid4Vc/CredentialSet/CredentialSetService.cs @@ -117,24 +117,23 @@ public virtual async Task UpdateAsync(CredentialSetRecord credentialSetRecord) public async Task RefreshCredentialSetState(CredentialSetRecord credentialSetRecord) { + var oldState = credentialSetRecord.State; + if (credentialSetRecord.IsDeleted()) return credentialSetRecord; - - await credentialSetRecord.ExpiresAt.IfSomeAsync(async expiresAt => + + credentialSetRecord.ExpiresAt.IfSome(expiresAt => { if (expiresAt < DateTime.UtcNow) - { credentialSetRecord.State = CredentialState.Expired; - await UpdateAsync(credentialSetRecord); - } }); //TODO: Implement revocation check (status List) -> Currently IsRevoked always returns false if (credentialSetRecord.IsRevoked()) - { credentialSetRecord.State = CredentialState.Revoked; + + if (oldState != credentialSetRecord.State) await UpdateAsync(credentialSetRecord); - } return credentialSetRecord; } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/AuthorizationDetails.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/AuthorizationDetails.cs index 6e728596..a38aebf3 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/AuthorizationDetails.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/AuthorizationDetails.cs @@ -38,21 +38,10 @@ public record AuthorizationDetails public string[]? Locations { get; } internal AuthorizationDetails( - string? format, - string? vct, - string? credentialConfigurationId, - string[]? locations, - string? docType) + string credentialConfigurationId, + string[]? locations) { - if (!string.IsNullOrWhiteSpace(format) && !string.IsNullOrWhiteSpace(credentialConfigurationId)) - { - throw new ArgumentException("Both format and credentialConfigurationId cannot be present at the same time."); - } - - Format = format; - Vct = vct; CredentialConfigurationId = credentialConfigurationId; Locations = locations; - DocType = docType; } } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/PushedAuthorizationRequest.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/PushedAuthorizationRequest.cs deleted file mode 100644 index 17c31f76..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/PushedAuthorizationRequest.cs +++ /dev/null @@ -1,106 +0,0 @@ -using Newtonsoft.Json; -using static Newtonsoft.Json.JsonConvert; - -namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; - -internal record PushedAuthorizationRequest -{ - [JsonProperty("client_id")] - public string ClientId { get; } - - [JsonProperty("response_type")] - public string ResponseType { get; } = "code"; - - [JsonProperty("redirect_uri")] - public string RedirectUri { get; } - - [JsonProperty("code_challenge")] - public string CodeChallenge { get; } - - [JsonProperty("code_challenge_method")] - public string CodeChallengeMethod { get; } - - [JsonProperty("state", NullValueHandling = NullValueHandling.Ignore)] - public string AuthFlowSessionState { get; } - - [JsonProperty("authorization_details", NullValueHandling = NullValueHandling.Ignore)] - public AuthorizationDetails[]? AuthorizationDetails { get; } - - [JsonProperty("issuer_state", NullValueHandling = NullValueHandling.Ignore)] - public string? IssuerState { get; } - - [JsonProperty("wallet_issuer", NullValueHandling = NullValueHandling.Ignore)] - public string? WalletIssuer { get; } - - [JsonProperty("user_hint", NullValueHandling = NullValueHandling.Ignore)] - public string? UserHint { get; } - - [JsonProperty("scope", NullValueHandling = NullValueHandling.Ignore)] - public string? Scope { get; } - - [JsonProperty("resource", NullValueHandling = NullValueHandling.Ignore)] - public string? Resource { get; } - - public PushedAuthorizationRequest( - AuthFlowSessionState authFlowSessionState, - ClientOptions clientOptions, - AuthorizationCodeParameters authorizationCodeParameters, - AuthorizationDetails[]? authorizationDetails, - string? scope, - string? issuerState, - string? userHint, - string? resource) - { - ClientId = clientOptions.ClientId; - RedirectUri = clientOptions.RedirectUri; - WalletIssuer = clientOptions.WalletIssuer; - CodeChallenge = authorizationCodeParameters.Challenge; - CodeChallengeMethod = authorizationCodeParameters.CodeChallengeMethod; - AuthFlowSessionState = authFlowSessionState; - AuthorizationDetails = authorizationDetails; - IssuerState = issuerState; - UserHint = userHint; - Scope = scope; - Resource = resource; - } - - public FormUrlEncodedContent ToFormUrlEncoded() - { - var keyValuePairs = new List>(); - - if (!string.IsNullOrEmpty(ClientId)) - keyValuePairs.Add(new KeyValuePair("client_id", ClientId)); - - if (!string.IsNullOrEmpty(ResponseType)) - keyValuePairs.Add(new KeyValuePair("response_type", ResponseType)); - - if (!string.IsNullOrEmpty(RedirectUri)) - keyValuePairs.Add(new KeyValuePair("redirect_uri", RedirectUri)); - - if (!string.IsNullOrEmpty(CodeChallenge)) - keyValuePairs.Add(new KeyValuePair("code_challenge", CodeChallenge)); - - if (!string.IsNullOrEmpty(CodeChallengeMethod)) - keyValuePairs.Add(new KeyValuePair("code_challenge_method", CodeChallengeMethod)); - - if (!string.IsNullOrEmpty(AuthFlowSessionState)) - keyValuePairs.Add(new KeyValuePair("state", AuthFlowSessionState)); - - if (AuthorizationDetails != null) - keyValuePairs.Add(new KeyValuePair("authorization_details", SerializeObject(AuthorizationDetails))); - - if (!string.IsNullOrEmpty(IssuerState)) - keyValuePairs.Add(new KeyValuePair("issuer_state", IssuerState)); - - if (!string.IsNullOrEmpty(WalletIssuer)) - keyValuePairs.Add(new KeyValuePair("wallet_issuer", WalletIssuer)); - - if (!string.IsNullOrEmpty(UserHint)) - keyValuePairs.Add(new KeyValuePair("user_hint", UserHint)); - - if (!string.IsNullOrEmpty(Scope)) - keyValuePairs.Add(new KeyValuePair("scope", Scope)); - - return new FormUrlEncodedContent(keyValuePairs); - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/VciAuthorizationRequest.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/VciAuthorizationRequest.cs new file mode 100644 index 00000000..f30cf1e3 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/VciAuthorizationRequest.cs @@ -0,0 +1,150 @@ +using System.Web; +using Newtonsoft.Json; +using static Newtonsoft.Json.JsonConvert; + +namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; + +internal record VciAuthorizationRequest +{ + [JsonProperty("client_id")] + public string ClientId { get; } + + [JsonProperty("response_type")] + public string ResponseType { get; } = "code"; + + [JsonProperty("redirect_uri")] + public string RedirectUri { get; } + + [JsonProperty("code_challenge")] + public string CodeChallenge { get; } + + [JsonProperty("code_challenge_method")] + public string CodeChallengeMethod { get; } + + [JsonProperty("state", NullValueHandling = NullValueHandling.Ignore)] + public string AuthFlowSessionState { get; } + + [JsonProperty("authorization_details", NullValueHandling = NullValueHandling.Ignore)] + public AuthorizationDetails[]? AuthorizationDetails { get; } + + [JsonProperty("issuer_state", NullValueHandling = NullValueHandling.Ignore)] + public string? IssuerState { get; } + + [JsonProperty("wallet_issuer", NullValueHandling = NullValueHandling.Ignore)] + public string? WalletIssuer { get; } + + [JsonProperty("user_hint", NullValueHandling = NullValueHandling.Ignore)] + public string? UserHint { get; } + + [JsonProperty("scope", NullValueHandling = NullValueHandling.Ignore)] + public string? Scope { get; } + + [JsonProperty("resource", NullValueHandling = NullValueHandling.Ignore)] + public string? Resource { get; } + + public VciAuthorizationRequest( + AuthFlowSessionState authFlowSessionState, + ClientOptions clientOptions, + AuthorizationCodeParameters authorizationCodeParameters, + AuthorizationDetails[]? authorizationDetails, + string? scope, + string? issuerState, + string? userHint, + string? resource) + { + ClientId = clientOptions.ClientId; + RedirectUri = clientOptions.RedirectUri; + WalletIssuer = clientOptions.WalletIssuer; + CodeChallenge = authorizationCodeParameters.Challenge; + CodeChallengeMethod = authorizationCodeParameters.CodeChallengeMethod; + AuthFlowSessionState = authFlowSessionState; + AuthorizationDetails = authorizationDetails; + IssuerState = issuerState; + UserHint = userHint; + Scope = scope; + Resource = resource; + } +} + +internal static class VciAuthorizationRequestExtensions +{ + internal static FormUrlEncodedContent ToFormUrlEncoded(this VciAuthorizationRequest authorizationRequest) + { + var keyValuePairs = new List>(); + + if (!string.IsNullOrEmpty(authorizationRequest.ClientId)) + keyValuePairs.Add(new KeyValuePair("client_id", authorizationRequest.ClientId)); + + if (!string.IsNullOrEmpty(authorizationRequest.ResponseType)) + keyValuePairs.Add(new KeyValuePair("response_type", authorizationRequest.ResponseType)); + + if (!string.IsNullOrEmpty(authorizationRequest.RedirectUri)) + keyValuePairs.Add(new KeyValuePair("redirect_uri", authorizationRequest.RedirectUri)); + + if (!string.IsNullOrEmpty(authorizationRequest.CodeChallenge)) + keyValuePairs.Add(new KeyValuePair("code_challenge", authorizationRequest.CodeChallenge)); + + if (!string.IsNullOrEmpty(authorizationRequest.CodeChallengeMethod)) + keyValuePairs.Add(new KeyValuePair("code_challenge_method", authorizationRequest.CodeChallengeMethod)); + + if (!string.IsNullOrEmpty(authorizationRequest.AuthFlowSessionState)) + keyValuePairs.Add(new KeyValuePair("state", authorizationRequest.AuthFlowSessionState)); + + if (authorizationRequest.AuthorizationDetails != null) + keyValuePairs.Add(new KeyValuePair("authorization_details", SerializeObject(authorizationRequest.AuthorizationDetails))); + + if (!string.IsNullOrEmpty(authorizationRequest.IssuerState)) + keyValuePairs.Add(new KeyValuePair("issuer_state", authorizationRequest.IssuerState)); + + if (!string.IsNullOrEmpty(authorizationRequest.WalletIssuer)) + keyValuePairs.Add(new KeyValuePair("wallet_issuer", authorizationRequest.WalletIssuer)); + + if (!string.IsNullOrEmpty(authorizationRequest.UserHint)) + keyValuePairs.Add(new KeyValuePair("user_hint", authorizationRequest.UserHint)); + + if (!string.IsNullOrEmpty(authorizationRequest.Scope)) + keyValuePairs.Add(new KeyValuePair("scope", authorizationRequest.Scope)); + + return new FormUrlEncodedContent(keyValuePairs); + } + + internal static string ToQueryString(this VciAuthorizationRequest authorizationRequest) + { + var queryString = "?"; + + if (!string.IsNullOrEmpty(authorizationRequest.ClientId)) + queryString = queryString + "client_id=" + authorizationRequest.ClientId + "&"; + + if (!string.IsNullOrEmpty(authorizationRequest.ResponseType)) + queryString = queryString + "response_type=" + authorizationRequest.ResponseType + "&"; + + if (!string.IsNullOrEmpty(authorizationRequest.RedirectUri)) + queryString = queryString + "redirect_uri=" + HttpUtility.UrlEncode(authorizationRequest.RedirectUri) + "&"; + + if (!string.IsNullOrEmpty(authorizationRequest.CodeChallenge)) + queryString = queryString + "code_challenge=" + authorizationRequest.CodeChallenge + "&"; + + if (!string.IsNullOrEmpty(authorizationRequest.CodeChallengeMethod)) + queryString = queryString + "code_challenge_method=" + authorizationRequest.CodeChallengeMethod + "&"; + + if (!string.IsNullOrEmpty(authorizationRequest.AuthFlowSessionState)) + queryString = queryString + "state=" + authorizationRequest.AuthFlowSessionState + "&"; + + if (authorizationRequest.AuthorizationDetails != null) + queryString = queryString + "authorization_details=" + HttpUtility.UrlEncode(SerializeObject(authorizationRequest.AuthorizationDetails)) + "&"; + + if (!string.IsNullOrEmpty(authorizationRequest.IssuerState)) + queryString = queryString + "issuer_state=" + authorizationRequest.IssuerState + "&"; + + if (!string.IsNullOrEmpty(authorizationRequest.WalletIssuer)) + queryString = queryString + "wallet_issuer=" + HttpUtility.UrlEncode(authorizationRequest.WalletIssuer) + "&"; + + if (!string.IsNullOrEmpty(authorizationRequest.UserHint)) + queryString = queryString + "user_hint=" + authorizationRequest.UserHint + "&"; + + if (!string.IsNullOrEmpty(authorizationRequest.Scope)) + queryString = queryString + "scope=" + authorizationRequest.Scope + "&"; + + return queryString.TrimEnd('&'); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/AuthorizationServerMetadata.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/AuthorizationServerMetadata.cs index c6b15430..3f6cf153 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/AuthorizationServerMetadata.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/AuthorizationServerMetadata.cs @@ -73,6 +73,16 @@ public class AuthorizationServerMetadata /// [JsonProperty("require_pushed_authorization_requests")] public bool? RequirePushedAuthorizationRequests { get; set; } + + /// + /// Gets or sets a value indicating which grant types the Authorization Server supports. + /// + [JsonProperty("grant_types_supported")] + public string[]? GrantTypesSupported { get; set; } internal bool IsDPoPSupported => DPopSigningAlgValuesSupported != null && DPopSigningAlgValuesSupported.Contains("ES256"); + + internal bool SupportsPreAuthFlow => GrantTypesSupported != null && GrantTypesSupported.Contains("urn:ietf:params:oauth:grant-type:pre-authorized_code"); + + internal bool SupportsAuthCodeFlow => GrantTypesSupported != null && GrantTypesSupported.Contains("authorization_code"); } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialDisplay.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialDisplay.cs index 150ce214..340be3e5 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialDisplay.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialDisplay.cs @@ -118,23 +118,4 @@ public static JObject EncodeToJson(this CredentialDisplay display) return result; } - - // TODO: Unpure - public static SdJwtDisplay ToSdJwtDisplay(this CredentialDisplay credentialDisplay) - { - var logo = new SdJwtDisplay.SdJwtLogo - { - Uri = credentialDisplay.Logo.ToNullable()?.Uri.ToNullable()!, - AltText = credentialDisplay.Logo.ToNullable()?.AltText.ToNullable() - }; - - return new SdJwtDisplay - { - Logo = logo, - Name = credentialDisplay.Name.ToNullable(), - BackgroundColor = credentialDisplay.BackgroundColor.ToNullable(), - Locale = credentialDisplay.Locale.ToNullable()?.ToString(), - TextColor = credentialDisplay.TextColor.ToNullable() - }; - } } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialLogo.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialLogo.cs index 99a9d079..4b1b6b3a 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialLogo.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialLogo.cs @@ -20,9 +20,9 @@ public record CredentialLogo /// /// Gets the URL of the logo image. /// - public Option Uri { get; } + public Uri Uri { get; } - private CredentialLogo(Option altText, Option uri) + private CredentialLogo(Uri uri, Option altText) { AltText = altText; Uri = uri; @@ -33,30 +33,21 @@ public static Option OptionalCredentialLogo(JToken logo) var altText = logo.GetByKey(AltTextJsonKey).ToOption().OnSome(text => { var str = text.ToString(); - if (string.IsNullOrWhiteSpace(str)) - return Option.None; - - return str; - }); - - var imageUri = logo.GetByKey(UriJsonKey).ToOption().OnSome(uri => - { - try - { - var str = uri.ToString(); - var result = new Uri(str); - return result; - } - catch (Exception) - { - return Option.None; - } + return string.IsNullOrWhiteSpace(str) ? Option.None : str; }); - if (altText.IsNone && imageUri.IsNone) - return Option.None; - - return new CredentialLogo(altText, imageUri); + return logo.GetByKey(UriJsonKey).ToOption().Match( + uri => { + try + { + return new CredentialLogo(new Uri(uri.ToString()), altText); + } + catch (Exception) + { + return Option.None; + } + }, + () => Option.None); } } @@ -74,10 +65,7 @@ public static JObject EncodeToJson(this CredentialLogo logo) result.Add(AltTextJsonKey, altText); }); - logo.Uri.IfSome(uri => - { - result.Add(UriJsonKey, uri.ToStringWithoutTrail()); - }); + result.Add(UriJsonKey, logo.Uri.ToStringWithoutTrail()); return result; } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/SdJwt/SdJwtConfiguration.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/SdJwt/SdJwtConfiguration.cs index dde72e7f..68a01737 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/SdJwt/SdJwtConfiguration.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/SdJwt/SdJwtConfiguration.cs @@ -83,4 +83,26 @@ public static JObject EncodeToJson(this SdJwtConfiguration config) return credentialConfig; } + + public static Dictionary ExtractClaimMetadata(this SdJwtConfiguration sdJwtConfiguration) + { + return sdJwtConfiguration + .Claims? + .Where(x => !string.IsNullOrWhiteSpace(x.Key) && x.Value.Display is not null) + .SelectMany(claimMetadata => + { + var claimMetadatas = new Dictionary { { claimMetadata.Key, claimMetadata.Value } }; + + if (claimMetadata.Value.NestedClaims == null || claimMetadata.Value.NestedClaims.Count == 0) + return claimMetadatas; + + foreach (var nested in claimMetadata.Value.NestedClaims!) + { + claimMetadatas.Add(claimMetadata.Key + "." + nested.Key, nested.Value?.ToObject()!); + } + + return claimMetadatas; + }) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value) ?? new Dictionary(); + } } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs index aaadd99b..fb6eae30 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs @@ -79,7 +79,7 @@ await issuerMetadata.BatchCredentialIssuance.Match( await batchCredentialIssuance.BatchSize.Match( Some: async batchSize => { - proofs = await GetProofsOfPossessionAsync(Math.Max(MaxBatchSize, batchSize), keyId, + proofs = await GetProofsOfPossessionAsync(Math.Min(MaxBatchSize, batchSize), keyId, issuerMetadata, cNonce, clientOptions); }, None: async () => diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/MdocFun.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/MdocFun.cs index 5ed02a39..61eb2095 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/MdocFun.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/MdocFun.cs @@ -19,9 +19,8 @@ from credentialDisplays in credentialConfigurationDisplay let mdocDisplays = credentialDisplays.Select(credentialDisplay => { var logo = - from credentialLogo in credentialDisplay.Logo - from uri in credentialLogo.Uri - select new MdocLogo(uri); + from credentialLogo in credentialDisplay.Logo + select new MdocLogo(credentialLogo.Uri); var mdocName = from credentialName in credentialDisplay.Name diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs index 048d889d..51330867 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs @@ -17,6 +17,7 @@ using WalletFramework.Core.Credentials.Abstractions; using WalletFramework.Core.Functional; using WalletFramework.Core.Localization; +using WalletFramework.Core.String; using WalletFramework.MdocLib; using WalletFramework.MdocVc; using WalletFramework.Oid4Vc.CredentialSet; @@ -112,18 +113,13 @@ public async Task InitiateAuthFlow(CredentialOfferMetadata offer, ClientOpt .Where(config => offer.CredentialOffer.CredentialConfigurationIds.Contains(config.Key)) .Select(pair => pair.Value.Match( sdJwt => new AuthorizationDetails( - null, - sdJwt.Vct.ToString(), pair.Key.ToString(), - issuerMetadata.AuthorizationServers.ToNullable()?.Select(id => id.ToString()).ToArray(), - null + issuerMetadata.AuthorizationServers.ToNullable()?.Select(id => id.ToString()).ToArray() ), mdoc => new AuthorizationDetails( - null, - null, pair.Key.ToString(), - issuerMetadata.AuthorizationServers.ToNullable()?.Select(id => id.ToString()).ToArray(), - mdoc.DocType.ToString())) + issuerMetadata.AuthorizationServers.ToNullable()?.Select(id => id.ToString()).ToArray()) + ) ); var authCode = @@ -136,7 +132,7 @@ from code in authCode from issState in code.IssuerState select issState; - var par = new PushedAuthorizationRequest( + var vciAuthorizationRequest = new VciAuthorizationRequest( sessionId, clientOptions, authorizationCodeParameters, @@ -145,22 +141,13 @@ from issState in code.IssuerState issuerState.ToNullable(), null, null); - + var authServerMetadata = await FetchAuthorizationServerMetadataAsync(issuerMetadata, offer.CredentialOffer); - - _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())); - + + var authorizationRequestUri = authServerMetadata.PushedAuthorizationRequestEndpoint.IsNullOrEmpty() + ? new Uri(authServerMetadata.AuthorizationEndpoint + "?" + vciAuthorizationRequest.ToQueryString()) + : await GetRequestUriUsingPushedAuthorizationRequest(authServerMetadata, vciAuthorizationRequest); + var authorizationData = new AuthorizationData( clientOptions, issuerMetadata, @@ -189,6 +176,9 @@ public async Task InitiateAuthFlow(Uri uri, ClientOptions clientOptions, Op return await issuerMetadata.Match( async validIssuerMetadata => { + var authServerMetadata = + await FetchAuthorizationServerMetadataAsync(validIssuerMetadata, Option.None); + var sessionId = AuthFlowSessionState.CreateAuthFlowSessionState(); var authorizationCodeParameters = CreateAndStoreCodeChallenge(); @@ -197,32 +187,29 @@ public async Task InitiateAuthFlow(Uri uri, ClientOptions clientOptions, Op mdDocConfig => mdDocConfig.CredentialConfiguration.Scope.OnSome(scope => scope.ToString()) ); - var par = new PushedAuthorizationRequest( + 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 vciAuthorizationRequest = new VciAuthorizationRequest( sessionId, clientOptions, authorizationCodeParameters, - null, + [authorizationDetails], scope.ToNullable(), null, null, null); - var authServerMetadata = - await FetchAuthorizationServerMetadataAsync(validIssuerMetadata, Option.None); - - _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())); - + var authorizationRequestUri = authServerMetadata.PushedAuthorizationRequestEndpoint.IsNullOrEmpty() + ? new Uri(authServerMetadata.AuthorizationEndpoint + "?" + vciAuthorizationRequest.ToQueryString()) + : await GetRequestUriUsingPushedAuthorizationRequest(authServerMetadata, vciAuthorizationRequest); + //TODO: Select multiple configurationIds var authorizationData = new AuthorizationData( clientOptions, @@ -244,6 +231,22 @@ await _authFlowSessionStorage.StoreAsync( ); } + private async Task GetRequestUriUsingPushedAuthorizationRequest(AuthorizationServerMetadata authorizationServerMetadata, VciAuthorizationRequest vciAuthorizationRequest) + { + _httpClient.DefaultRequestHeaders.Clear(); + var response = await _httpClient.PostAsync( + authorizationServerMetadata.PushedAuthorizationRequestEndpoint, + vciAuthorizationRequest.ToFormUrlEncoded() + ); + + var parResponse = DeserializeObject(await response.Content.ReadAsStringAsync()) + ?? throw new InvalidOperationException("Failed to deserialize the PAR response."); + + return new Uri(authorizationServerMetadata.AuthorizationEndpoint + + "?client_id=" + vciAuthorizationRequest.ClientId + + "&request_uri=" + System.Net.WebUtility.UrlEncode(parResponse.RequestUri.ToString())); + } + public async Task> AcceptOffer(CredentialOfferMetadata credentialOfferMetadata, string? transactionCode) { var issuerMetadata = credentialOfferMetadata.IssuerMetadata; @@ -294,7 +297,6 @@ await credential.Value.Match( await _sdJwtService.AddAsync(context, record); credentialSet.AddSdJwtData(record); - await _credentialSetService.AddAsync(credentialSet); }, async mdoc => { @@ -304,14 +306,17 @@ await credential.Value.Match( await _mdocStorage.Add(record); credentialSet.AddMDocData(record); - await _credentialSetService.AddAsync(credentialSet); }); } }, // ReSharper disable once UnusedParameter.Local transactionId => throw new NotImplementedException()); - - await result.OnSuccess(task => task); + + await result.OnSuccess(async task => + { + await task; + await _credentialSetService.AddAsync(credentialSet); + }); return credentialSet; } @@ -592,7 +597,7 @@ private async Task FetchAuthorizationServerMetadata { Uri credentialIssuer = issuerMetadata.CredentialIssuer; - var authServerUrl = issuerMetadata.AuthorizationServers.Match( + var authServerUrls = issuerMetadata.AuthorizationServers.Match( issuerMetadataAuthServers => { var credentialOfferAuthServer = from offer in credentialOffer @@ -605,30 +610,64 @@ from server in code.AuthorizationServer offerAuthServer => { var matchingAuthServer = issuerMetadataAuthServers.Find(issuerMetadataAuthServer => issuerMetadataAuthServer.ToString() == offerAuthServer); - + return matchingAuthServer.Match( - Some: server => CreateAuthorizationServerMetadataUri(server), + Some: server => new List(){CreateAuthorizationServerMetadataUri(server)}, None: () => throw new InvalidOperationException( "The authorization server in the credential offer does not match any authorization server in the issuer metadata.")); }, - () => CreateAuthorizationServerMetadataUri(issuerMetadataAuthServers.First())); + () => issuerMetadataAuthServers.Select(uri => CreateAuthorizationServerMetadataUri(uri)) + ); }, - () => CreateAuthorizationServerMetadataUri(credentialIssuer)); + () => new List(){CreateAuthorizationServerMetadataUri(credentialIssuer)}); - var getAuthServerResponse = await _httpClient.GetAsync(authServerUrl); - if (!getAuthServerResponse.IsSuccessStatusCode) - throw new HttpRequestException( - $"Failed to get authorization server metadata. Status Code is: {getAuthServerResponse.StatusCode}" - ); + var authorizationServerMetadatas = new List(); + foreach (var authServerUrl in authServerUrls) + { + var getAuthServerResponse = await _httpClient.GetAsync(authServerUrl); + + if (!getAuthServerResponse.IsSuccessStatusCode) + throw new HttpRequestException( + $"Failed to get authorization server metadata. Status Code is: {getAuthServerResponse.StatusCode}" + ); + + var content = await getAuthServerResponse.Content.ReadAsStringAsync(); + + var authServer = DeserializeObject(content) + ?? throw new InvalidOperationException( + "Failed to deserialize the authorization server metadata."); + + authorizationServerMetadatas.Add(authServer); + } - var content = await getAuthServerResponse.Content.ReadAsStringAsync(); + if (authorizationServerMetadatas.Count == 1) + return authorizationServerMetadatas.First(); - var authServer = DeserializeObject(content) - ?? throw new InvalidOperationException( - "Failed to deserialize the authorization server metadata."); + return credentialOffer.Match( + Some: offer => + { + var credentialOfferAuthCodeGrantType = from grants in offer.Grants + from code in grants.AuthorizationCode + select code; - return authServer; + return credentialOfferAuthCodeGrantType.Match( + Some: code => authorizationServerMetadatas.Find(authServer => authServer.SupportsAuthCodeFlow) ?? + throw new InvalidOperationException("No suitable Authorization Server found"), + None: () => + { + var credentialOfferPreAuthGrantType = from grants in offer.Grants + from code in grants.AuthorizationCode + select code; + + return credentialOfferPreAuthGrantType.Match( + Some: preAuth => authorizationServerMetadatas.Find(authServer => authServer.SupportsPreAuthFlow) + ?? throw new InvalidOperationException("No suitable Authorization Server found"), + None: () => authorizationServerMetadatas.First()); + }); + }, + None: () => authorizationServerMetadatas.Find(authServer => authServer.SupportsAuthCodeFlow) + ?? throw new InvalidOperationException("No suitable Authorization Server found")); } private static Uri CreateAuthorizationServerMetadataUri(Uri authorizationServerUri) diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/SdJwtRecordExtensions.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/SdJwtRecordExtensions.cs index 9dfb47ed..25fcfded 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/SdJwtRecordExtensions.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/SdJwtRecordExtensions.cs @@ -19,23 +19,7 @@ public static SdJwtRecord ToRecord( CredentialSetId credentialSetId, bool isOneTimeUse) { - var claims = configuration - .Claims? - .SelectMany(claimMetadata => - { - var claimMetadatas = new Dictionary { { claimMetadata.Key, claimMetadata.Value } }; - - if (!(claimMetadata.Value.NestedClaims == null || claimMetadata.Value.NestedClaims.Count == 0)) - { - foreach (var nested in claimMetadata.Value.NestedClaims!) - { - claimMetadatas.Add(claimMetadata.Key + "." + nested.Key, nested.Value?.ToObject()!); - } - } - - return claimMetadatas; - }) - .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + var claims = configuration.ExtractClaimMetadata(); var display = from displays in configuration.CredentialConfiguration.Display @@ -49,7 +33,7 @@ select displays.Select(credentialDisplay => Logo = new SdJwtDisplay.SdJwtLogo { AltText = credentialDisplay.Logo.ToNullable()?.AltText.ToNullable(), - Uri = credentialDisplay.Logo.ToNullable()?.Uri.ToNullable()! + Uri = credentialDisplay.Logo.ToNullable()?.Uri! }, Name = credentialDisplay.Name.ToNullable(), BackgroundColor = backgroundColor, diff --git a/src/WalletFramework.SdJwtVc/Services/VctMetadataService.cs b/src/WalletFramework.SdJwtVc/Services/VctMetadataService.cs index 88274a6f..4e213cdd 100644 --- a/src/WalletFramework.SdJwtVc/Services/VctMetadataService.cs +++ b/src/WalletFramework.SdJwtVc/Services/VctMetadataService.cs @@ -33,7 +33,7 @@ public async Task> ProcessMetadata(Vct vct) try { - var response = await _httpClient.GetAsync(metadataUrl); + var response = await _httpClient.GetAsync(metadataUrl); if (response.IsSuccessStatusCode) { var str = await response.Content.ReadAsStringAsync();