Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add wallet initiated Auth Flow #146

Merged
merged 3 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions src/WalletFramework.MdocLib/IssuerSigned.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,10 @@ private IssuerSigned(NameSpaces nameSpaces, IssuerAuth issuerAuth)
private static IssuerSigned Create(NameSpaces nameSpaces, IssuerAuth issuerAuth) =>
new(nameSpaces, issuerAuth);

internal static Validation<IssuerSigned> ValidIssuerSigned(CBORObject mdoc) =>
mdoc.GetByLabel(IssuerSignedLabel).OnSuccess(issuerSigned =>
Valid(Create)
.Apply(ValidNameSpaces(issuerSigned))
.Apply(ValidIssuerAuth(issuerSigned))
);
public static Validation<IssuerSigned> ValidIssuerSigned(CBORObject issuerSigned) =>
Valid(Create)
.Apply(ValidNameSpaces(issuerSigned))
.Apply(ValidIssuerAuth(issuerSigned));

public CBORObject Encode()
{
Expand All @@ -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);
}
}
47 changes: 45 additions & 2 deletions src/WalletFramework.MdocLib/Mdoc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -68,12 +68,55 @@ public static Validation<Mdoc> 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<Mdoc> FromIssuerSigned(string base64UrlencodedCborByteString)
{
var decodeBase64Url = new Func<string, Validation<byte[]>>(str =>
{
try
{
return Base64UrlEncoder.DecodeBytes(str)!;
}
catch (Exception e)
{
return new InvalidBase64UrlEncodingError(e);
}
});

var parseCborByteString = new Func<byte[], Validation<CBORObject>>(bytes =>
{
try
{
return CBORObject.DecodeFromBytes(bytes);
}
catch (Exception e)
{
return new InvalidCborByteStringError("mdocResponse", e);
}
});

var validateIntegrity = new List<Validator<Mdoc>>
{
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);
}
Expand Down
4 changes: 2 additions & 2 deletions src/WalletFramework.MdocVc/MdocRecord.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,21 @@ namespace WalletFramework.Oid4Vc.Oid4Vci.Abstractions;
public interface IOid4VciClientService
{
/// <summary>
/// Initiates the authorization process of the VCI authorization code flow.
/// Initiates the issuer initiated authorization process of the VCI authorization code flow.
/// </summary>
/// <param name="offer">The offer metadata</param>
/// <param name="clientOptions">The client options</param>
/// <returns></returns>
Task<Uri> InitiateAuthFlow(CredentialOfferMetadata offer, ClientOptions clientOptions);

/// <summary>
/// Initiates the wallet initiate authorization process of the VCI authorization code flow.
/// </summary>
/// <param name="uri">The issuers uri</param>
/// <param name="clientOptions">The client options</param>
/// <param name="language">Optional language tag</param>
/// <returns></returns>
Task<Uri> InitiateAuthFlow(Uri uri, ClientOptions clientOptions, Option<Locale> language);

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

/// <summary>
/// Processes a credential offer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ public async Task<DPopHttpResponse> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
public class OAuthToken
public record OAuthToken
{
/// <summary>
/// Indicates if the Token Request is still pending as the Credential Issuer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public static Validation<EncodedMdoc> ValidEncodedMdoc(JValue mdoc)
var str = mdoc.ToString(CultureInfo.InvariantCulture);

return MdocLib.Mdoc
.ValidMdoc(str)
.FromIssuerSigned(str)
.OnSuccess(mdoc1 => new EncodedMdoc(str, mdoc1));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -167,6 +168,71 @@ await _authFlowSessionStorage.StoreAsync(
return authorizationRequestUri;
}

public async Task<Uri> InitiateAuthFlow(Uri uri, ClientOptions clientOptions, Option<Locale> 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<PushedAuthorizationRequestResponse>(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,
validIssuerMetadata.CredentialConfigurationsSupported.Keys.ToList());

var context = await _agentProvider.GetContextAsync();
await _authFlowSessionStorage.StoreAsync(
context,
authorizationData,
authorizationCodeParameters,
sessionId);

return authorizationRequestUri;
},
_ => throw new Exception("Fetching Issuer metadata failed")
);
}

public async Task<Validation<OneOf<SdJwtRecord, MdocRecord>>> AcceptOffer(CredentialOfferMetadata credentialOfferMetadata, string? transactionCode)
{
var issuerMetadata = credentialOfferMetadata.IssuerMetadata;
Expand Down Expand Up @@ -206,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 =>
Expand Down Expand Up @@ -237,7 +303,7 @@ from metadata in _issuerMetadataService.ProcessMetadata(offer.CredentialIssuer,
}

/// <inheritdoc />
public async Task<Validation<OneOf<SdJwtRecord, MdocRecord>>> RequestCredential(IssuanceSession issuanceSession)
public async Task<Validation<List<OneOf<SdJwtRecord, MdocRecord>>>> RequestCredential(IssuanceSession issuanceSession)
{
var context = await _agentProvider.GetContextAsync();

Expand All @@ -248,52 +314,75 @@ public async Task<Validation<OneOf<SdJwtRecord, MdocRecord>>> RequestCredential(
.IssuerMetadata
.CredentialConfigurationsSupported
.Where(config => session.AuthorizationData.CredentialConfigurationIds.Contains(config.Key))
.Select(pair => pair.Value)
.First();
.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()));

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
};

var token = await _tokenService.RequestToken(
tokenRequest,
session.AuthorizationData.AuthorizationServerMetadata);

var validResponse = await _credentialRequestService.RequestCredentials(
credConfiguration,
session.AuthorizationData.IssuerMetadata,
token,
session.AuthorizationData.ClientOptions);

List<OneOf<SdJwtRecord, MdocRecord>> 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<Task<OneOf<SdJwtRecord, MdocRecord>>>(
async sdJwt =>
{
token = token.Match<OneOf<OAuthToken, DPopToken>>(
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<OneOf<OAuthToken, DPopToken>>(
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<Task<OneOf<SdJwtRecord, MdocRecord>>>(
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()
Expand Down
Loading
Loading