Skip to content

Commit d314c94

Browse files
authored
Add wallet initiated Auth Flow (#146)
* add issuer initiated auth flow Signed-off-by: Johannes Tuerk <johannes.tuerk@lissi.id> * add workarounds for mdoc to allow malformed strucure for now Signed-off-by: Johannes Tuerk <johannes.tuerk@lissi.id> * support sdJwt and mDoc Cred Request at once Signed-off-by: Johannes Tuerk <johannes.tuerk@lissi.id> --------- Signed-off-by: Johannes Tuerk <johannes.tuerk@lissi.id>
1 parent 21b8639 commit d314c94

File tree

12 files changed

+217
-60
lines changed

12 files changed

+217
-60
lines changed

src/WalletFramework.MdocLib/IssuerSigned.cs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,10 @@ private IssuerSigned(NameSpaces nameSpaces, IssuerAuth issuerAuth)
2222
private static IssuerSigned Create(NameSpaces nameSpaces, IssuerAuth issuerAuth) =>
2323
new(nameSpaces, issuerAuth);
2424

25-
internal static Validation<IssuerSigned> ValidIssuerSigned(CBORObject mdoc) =>
26-
mdoc.GetByLabel(IssuerSignedLabel).OnSuccess(issuerSigned =>
27-
Valid(Create)
28-
.Apply(ValidNameSpaces(issuerSigned))
29-
.Apply(ValidIssuerAuth(issuerSigned))
30-
);
25+
public static Validation<IssuerSigned> ValidIssuerSigned(CBORObject issuerSigned) =>
26+
Valid(Create)
27+
.Apply(ValidNameSpaces(issuerSigned))
28+
.Apply(ValidIssuerAuth(issuerSigned));
3129

3230
public CBORObject Encode()
3331
{
@@ -39,3 +37,13 @@ public CBORObject Encode()
3937
return cbor;
4038
}
4139
}
40+
41+
public static class IssuerSignedFun
42+
{
43+
// TODO: This is only a hack currently, the doctype of the mdoc and the mso must be validated normally
44+
public static Mdoc ToMdoc(this IssuerSigned issuerSigned)
45+
{
46+
var docType = issuerSigned.IssuerAuth.Payload.DocType;
47+
return new Mdoc(docType, issuerSigned);
48+
}
49+
}

src/WalletFramework.MdocLib/Mdoc.cs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public record Mdoc
2020
// TODO: mdoc authentication
2121
// public DeviceSigned DeviceSigned { get; }
2222

23-
private Mdoc(DocType docType, IssuerSigned issuerSigned)
23+
public Mdoc(DocType docType, IssuerSigned issuerSigned)
2424
{
2525
DocType = docType;
2626
IssuerSigned = issuerSigned;
@@ -68,12 +68,55 @@ public static Validation<Mdoc> ValidMdoc(string base64UrlencodedCborByteString)
6868
return
6969
from bytes in decodeBase64Url(base64UrlencodedCborByteString)
7070
from cbor in parseCborByteString(bytes)
71+
from issuerSigned in cbor.GetByLabel(IssuerSignedLabel)
7172
from mdoc in Valid(Create)
7273
.Apply(ValidDoctype(cbor))
73-
.Apply(ValidIssuerSigned(cbor))
74+
.Apply(ValidIssuerSigned(issuerSigned))
7475
from validMdoc in validateIntegrity(mdoc)
7576
select validMdoc;
7677
}
78+
79+
//TODO: Workaround because PId Issuer only implemented issuer signed, Delete this overload when PID Issuer is fixed!!
80+
public static Validation<Mdoc> FromIssuerSigned(string base64UrlencodedCborByteString)
81+
{
82+
var decodeBase64Url = new Func<string, Validation<byte[]>>(str =>
83+
{
84+
try
85+
{
86+
return Base64UrlEncoder.DecodeBytes(str)!;
87+
}
88+
catch (Exception e)
89+
{
90+
return new InvalidBase64UrlEncodingError(e);
91+
}
92+
});
93+
94+
var parseCborByteString = new Func<byte[], Validation<CBORObject>>(bytes =>
95+
{
96+
try
97+
{
98+
return CBORObject.DecodeFromBytes(bytes);
99+
}
100+
catch (Exception e)
101+
{
102+
return new InvalidCborByteStringError("mdocResponse", e);
103+
}
104+
});
105+
106+
var validateIntegrity = new List<Validator<Mdoc>>
107+
{
108+
MdocFun.DocTypeMatches,
109+
MdocFun.DigestsMatch
110+
}
111+
.AggregateValidators();
112+
113+
return
114+
from bytes in decodeBase64Url(base64UrlencodedCborByteString)
115+
from cbor in parseCborByteString(bytes)
116+
from issuerSigned in ValidIssuerSigned(cbor)
117+
from validMdoc in validateIntegrity(issuerSigned.ToMdoc())
118+
select validMdoc;
119+
}
77120

78121
public record InvalidBase64UrlEncodingError(Exception E) : Error("String is not Base64UrlEncoded", E);
79122
}

src/WalletFramework.MdocVc/MdocRecord.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,12 @@ public static class MdocRecordFun
7575
public static MdocRecord DecodeFromJson(JObject json)
7676
{
7777
var id = json[nameof(RecordBase.Id)]!.ToString();
78-
78+
7979
var mdocStr = json[MdocJsonKey]!.ToString();
8080
var mdoc = Mdoc
8181
.ValidMdoc(mdocStr)
8282
.UnwrapOrThrow(new InvalidOperationException($"The MdocRecord with ID: {id} is corrupt"));
83-
83+
8484
var displays =
8585
from jToken in json.GetByKey(MdocDisplaysJsonKey).ToOption()
8686
from jArray in jToken.ToJArray().ToOption()

src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,21 @@ namespace WalletFramework.Oid4Vc.Oid4Vci.Abstractions;
1515
public interface IOid4VciClientService
1616
{
1717
/// <summary>
18-
/// Initiates the authorization process of the VCI authorization code flow.
18+
/// Initiates the issuer initiated authorization process of the VCI authorization code flow.
1919
/// </summary>
2020
/// <param name="offer">The offer metadata</param>
2121
/// <param name="clientOptions">The client options</param>
2222
/// <returns></returns>
2323
Task<Uri> InitiateAuthFlow(CredentialOfferMetadata offer, ClientOptions clientOptions);
24+
25+
/// <summary>
26+
/// Initiates the wallet initiate authorization process of the VCI authorization code flow.
27+
/// </summary>
28+
/// <param name="uri">The issuers uri</param>
29+
/// <param name="clientOptions">The client options</param>
30+
/// <param name="language">Optional language tag</param>
31+
/// <returns></returns>
32+
Task<Uri> InitiateAuthFlow(Uri uri, ClientOptions clientOptions, Option<Locale> language);
2433

2534
/// <summary>
2635
/// Requests a verifiable credential using the authorization code flow.
@@ -29,7 +38,7 @@ public interface IOid4VciClientService
2938
/// <returns>
3039
/// A list of credentials.
3140
/// </returns>
32-
Task<Validation<OneOf<SdJwtRecord, MdocRecord>>> RequestCredential(IssuanceSession issuanceSession);
41+
Task<Validation<List<OneOf<SdJwtRecord, MdocRecord>>>> RequestCredential(IssuanceSession issuanceSession);
3342

3443
/// <summary>
3544
/// Processes a credential offer

src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Implementations/DPopHttpClient.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ public async Task<DPopHttpResponse> Post(
7171
httpClient.WithDPopHeader(newDpop);
7272

7373
response = await httpClient.PostAsync(requestUri, getContent());
74+
75+
config = response.Headers.TryGetValues("DPoP-Nonce", out var refreshedDpopNonce)
76+
? config with { Nonce = new DPopNonce(refreshedDpopNonce?.First()!)}
77+
: config;
7478
}
7579

7680
await ThrowIfInvalidGrantError(response);

src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Models/DPopToken.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ namespace WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Models;
44

55
public record DPopToken
66
{
7-
internal OAuthToken Token { get; }
7+
internal OAuthToken Token { get; init; }
88

99
internal DPop DPop { get; }
1010

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models;
77
/// Represents a successful response from the OAuth 2.0 Authorization Server containing
88
/// the issued access token and related information.
99
/// </summary>
10-
public class OAuthToken
10+
public record OAuthToken
1111
{
1212
/// <summary>
1313
/// Indicates if the Token Request is still pending as the Credential Issuer

src/WalletFramework.Oid4Vc/Oid4Vci/CredResponse/Mdoc/EncodedMdoc.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public static Validation<EncodedMdoc> ValidEncodedMdoc(JValue mdoc)
2525
var str = mdoc.ToString(CultureInfo.InvariantCulture);
2626

2727
return MdocLib.Mdoc
28-
.ValidMdoc(str)
28+
.FromIssuerSigned(str)
2929
.OnSuccess(mdoc1 => new EncodedMdoc(str, mdoc1));
3030
}
3131
}

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

Lines changed: 121 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using WalletFramework.Core.Functional;
1818
using WalletFramework.Core.Localization;
1919
using WalletFramework.MdocVc;
20+
using WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Models;
2021
using WalletFramework.SdJwtVc.Models.Records;
2122
using WalletFramework.SdJwtVc.Services.SdJwtVcHolderService;
2223
using static Newtonsoft.Json.JsonConvert;
@@ -167,6 +168,71 @@ await _authFlowSessionStorage.StoreAsync(
167168
return authorizationRequestUri;
168169
}
169170

171+
public async Task<Uri> InitiateAuthFlow(Uri uri, ClientOptions clientOptions, Option<Locale> language)
172+
{
173+
var locale = language.Match(
174+
some => some,
175+
() => Constants.DefaultLocale);
176+
177+
var issuerMetadata = _issuerMetadataService.ProcessMetadata(uri, locale);
178+
179+
return await issuerMetadata.Match(
180+
async validIssuerMetadata =>
181+
{
182+
var sessionId = AuthFlowSessionState.CreateAuthFlowSessionState();
183+
var authorizationCodeParameters = CreateAndStoreCodeChallenge();
184+
185+
var scope = validIssuerMetadata.CredentialConfigurationsSupported.First().Value.Match(
186+
sdJwtConfig => sdJwtConfig.CredentialConfiguration.Scope.OnSome(scope => scope.ToString()),
187+
mdDocConfig => mdDocConfig.CredentialConfiguration.Scope.OnSome(scope => scope.ToString())
188+
);
189+
190+
var par = new PushedAuthorizationRequest(
191+
sessionId,
192+
clientOptions,
193+
authorizationCodeParameters,
194+
null,
195+
scope.ToNullable(),
196+
null,
197+
null,
198+
null);
199+
200+
var authServerMetadata =
201+
await FetchAuthorizationServerMetadataAsync(validIssuerMetadata);
202+
203+
_httpClient.DefaultRequestHeaders.Clear();
204+
var response = await _httpClient.PostAsync(
205+
authServerMetadata.PushedAuthorizationRequestEndpoint,
206+
par.ToFormUrlEncoded()
207+
);
208+
209+
var parResponse = DeserializeObject<PushedAuthorizationRequestResponse>(await response.Content.ReadAsStringAsync())
210+
?? throw new InvalidOperationException("Failed to deserialize the PAR response.");
211+
212+
var authorizationRequestUri = new Uri(authServerMetadata.AuthorizationEndpoint
213+
+ "?client_id=" + par.ClientId
214+
+ "&request_uri=" + System.Net.WebUtility.UrlEncode(parResponse.RequestUri.ToString()));
215+
216+
//TODO: Select multiple configurationIds
217+
var authorizationData = new AuthorizationData(
218+
clientOptions,
219+
validIssuerMetadata,
220+
authServerMetadata,
221+
validIssuerMetadata.CredentialConfigurationsSupported.Keys.ToList());
222+
223+
var context = await _agentProvider.GetContextAsync();
224+
await _authFlowSessionStorage.StoreAsync(
225+
context,
226+
authorizationData,
227+
authorizationCodeParameters,
228+
sessionId);
229+
230+
return authorizationRequestUri;
231+
},
232+
_ => throw new Exception("Fetching Issuer metadata failed")
233+
);
234+
}
235+
170236
public async Task<Validation<OneOf<SdJwtRecord, MdocRecord>>> AcceptOffer(CredentialOfferMetadata credentialOfferMetadata, string? transactionCode)
171237
{
172238
var issuerMetadata = credentialOfferMetadata.IssuerMetadata;
@@ -206,7 +272,7 @@ select credentialOrTransactionId.Match(
206272
{
207273
var record = sdJwt.Decoded.ToRecord(configuration.AsT0, issuerMetadata, response.KeyId);
208274
var context = await _agentProvider.GetContextAsync();
209-
await _sdJwtService.SaveAsync(context, record);
275+
await _sdJwtService.AddAsync(context, record);
210276
return record;
211277
},
212278
async mdoc =>
@@ -237,7 +303,7 @@ from metadata in _issuerMetadataService.ProcessMetadata(offer.CredentialIssuer,
237303
}
238304

239305
/// <inheritdoc />
240-
public async Task<Validation<OneOf<SdJwtRecord, MdocRecord>>> RequestCredential(IssuanceSession issuanceSession)
306+
public async Task<Validation<List<OneOf<SdJwtRecord, MdocRecord>>>> RequestCredential(IssuanceSession issuanceSession)
241307
{
242308
var context = await _agentProvider.GetContextAsync();
243309

@@ -248,52 +314,75 @@ public async Task<Validation<OneOf<SdJwtRecord, MdocRecord>>> RequestCredential(
248314
.IssuerMetadata
249315
.CredentialConfigurationsSupported
250316
.Where(config => session.AuthorizationData.CredentialConfigurationIds.Contains(config.Key))
251-
.Select(pair => pair.Value)
252-
.First();
317+
.Select(pair => pair.Value);
318+
319+
var scope = session
320+
.AuthorizationData
321+
.IssuerMetadata
322+
.CredentialConfigurationsSupported.First().Value.Match(
323+
sdJwtConfig => sdJwtConfig.CredentialConfiguration.Scope.OnSome(scope => scope.ToString()),
324+
mdDocConfig => mdDocConfig.CredentialConfiguration.Scope.OnSome(scope => scope.ToString()));
253325

254326
var tokenRequest = new TokenRequest
255327
{
256328
GrantType = AuthorizationCodeGrantTypeIdentifier,
257329
RedirectUri = session.AuthorizationData.ClientOptions.RedirectUri,
258330
CodeVerifier = session.AuthorizationCodeParameters.Verifier,
259331
Code = issuanceSession.Code,
332+
Scope = scope.ToNullable(),
260333
ClientId = session.AuthorizationData.ClientOptions.ClientId
261334
};
262335

263336
var token = await _tokenService.RequestToken(
264337
tokenRequest,
265338
session.AuthorizationData.AuthorizationServerMetadata);
266-
267-
var validResponse = await _credentialRequestService.RequestCredentials(
268-
credConfiguration,
269-
session.AuthorizationData.IssuerMetadata,
270-
token,
271-
session.AuthorizationData.ClientOptions);
339+
340+
List<OneOf<SdJwtRecord, MdocRecord>> credentials = new();
341+
//TODO: Make sure that it does not always request all available credConfigurations
342+
foreach (var configuration in credConfiguration)
343+
{
344+
var validResponse = await _credentialRequestService.RequestCredentials(
345+
configuration,
346+
session.AuthorizationData.IssuerMetadata,
347+
token,
348+
session.AuthorizationData.ClientOptions);
349+
350+
var result =
351+
from response in validResponse
352+
let cNonce = response.CNonce
353+
let credentialOrTransactionId = response.CredentialOrTransactionId
354+
select credentialOrTransactionId.Match(
355+
async credential => await credential.Value.Match<Task<OneOf<SdJwtRecord, MdocRecord>>>(
356+
async sdJwt =>
357+
{
358+
token = token.Match<OneOf<OAuthToken, DPopToken>>(
359+
oAuth => oAuth with { CNonce = cNonce.ToNullable()},
360+
dPop => dPop with { Token = dPop.Token with {CNonce = cNonce.ToNullable()}});
361+
362+
var record = sdJwt.Decoded.ToRecord(configuration.AsT0, session.AuthorizationData.IssuerMetadata, response.KeyId);
363+
await _sdJwtService.AddAsync(context, record);
364+
return record;
365+
},
366+
async mdoc =>
367+
{
368+
token = token.Match<OneOf<OAuthToken, DPopToken>>(
369+
oAuth => oAuth with { CNonce = cNonce.ToNullable()},
370+
dPop => dPop with { Token = dPop.Token with {CNonce = cNonce.ToNullable()}});
371+
372+
var displays = MdocFun.CreateMdocDisplays(configuration.AsT1);
373+
var record = mdoc.Decoded.ToRecord(displays);
374+
await _mdocStorage.Add(record);
375+
return record;
376+
}),
377+
// ReSharper disable once UnusedParameter.Local
378+
transactionId => throw new NotImplementedException());
379+
380+
await result.OnSuccess(async task => credentials.Add(await task));
381+
}
272382

273383
await _authFlowSessionStorage.DeleteAsync(context, session.AuthFlowSessionState);
274384

275-
var result =
276-
from response in validResponse
277-
let credentialOrTransactionId = response.CredentialOrTransactionId
278-
select credentialOrTransactionId.Match(
279-
async credential => await credential.Value.Match<Task<OneOf<SdJwtRecord, MdocRecord>>>(
280-
async sdJwt =>
281-
{
282-
var record = sdJwt.Decoded.ToRecord(credConfiguration.AsT0, session.AuthorizationData.IssuerMetadata, response.KeyId);
283-
await _sdJwtService.SaveAsync(context, record);
284-
return record;
285-
},
286-
async mdoc =>
287-
{
288-
var displays = MdocFun.CreateMdocDisplays(credConfiguration.AsT1);
289-
var record = mdoc.Decoded.ToRecord(displays);
290-
await _mdocStorage.Add(record);
291-
return record;
292-
}),
293-
// ReSharper disable once UnusedParameter.Local
294-
transactionId => throw new NotImplementedException());
295-
296-
return await result.OnSuccess(task => task);
385+
return credentials;
297386
}
298387

299388
private static AuthorizationCodeParameters CreateAndStoreCodeChallenge()

0 commit comments

Comments
 (0)