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

feat: attachments support #1738

Merged
merged 8 commits into from
Jul 2, 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,5 @@ cmd/vc-rest/.env
test/stress/cmd/.env

test/stress/.env

component/wallet-cli/wallet-cli
1 change: 1 addition & 0 deletions cmd/vc-rest/startcmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -958,6 +958,7 @@ func buildEchoHandler(
ResponseURI: conf.StartupParameters.apiGatewayURL + oidc4VPCheckEndpoint,
TokenLifetime: 15 * time.Minute,
Metrics: metrics,
AttachmentService: oidc4vp.NewAttachmentService(getHTTPClient(metricsProvider.Attachments)),
})

if conf.IsTraceEnabled {
Expand Down
1 change: 1 addition & 0 deletions pkg/observability/metrics/prometheus/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ func NewMetrics(
metrics.ClientDiscoverableClientIDScheme,
metrics.ClientAttestationService,
metrics.TrustRegistryService,
metrics.Attachments,
}

pm := &PromMetrics{
Expand Down
1 change: 1 addition & 0 deletions pkg/observability/metrics/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type ClientID string
const (
ClientPreAuth ClientID = "preauthorize"
ClientIssuerProfile ClientID = "issuer-profile"
Attachments ClientID = "attachments"
ClientVerifierProfile ClientID = "verifier-profile"
ClientCredentialStatus ClientID = "credential-status" //nolint:gosec
ClientOIDC4CI ClientID = "oidc4ci"
Expand Down
12 changes: 9 additions & 3 deletions pkg/service/oidc4vp/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,10 @@ type CredentialMetadata struct {
ExpirationDate *util.TimeWrapper `json:"expirationDate,omitempty"`
CustomClaims map[string]Claims `json:"customClaims,omitempty"`

Name interface{} `json:"name,omitempty"`
AwardedDate interface{} `json:"awardedDate,omitempty"`
Description interface{} `json:"description,omitempty"`
Name interface{} `json:"name,omitempty"`
AwardedDate interface{} `json:"awardedDate,omitempty"`
Description interface{} `json:"description,omitempty"`
Attachments []map[string]interface{} `json:"attachments"`
}

type ServiceInterface interface {
Expand Down Expand Up @@ -150,3 +151,8 @@ type ClientMetadata struct {
ClientPurpose string `json:"client_purpose"`
LogoURI string `json:"logo_uri"`
}

type Attachment struct {
Type string
Claim map[string]interface{}
}
191 changes: 191 additions & 0 deletions pkg/service/oidc4vp/attachments.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/*
Copyright SecureKey Technologies Inc. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

//go:generate mockgen -destination attachments_mocks_test.go -self_package mocks -package oidc4vp_test -source=attachments.go -mock_names httpClient=MockHttpClient

package oidc4vp

import (
"context"
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"sync"

"github.com/samber/lo"
"github.com/trustbloc/vc-go/util/maphelpers"
"github.com/trustbloc/vc-go/verifiable"
)

const (
AttachmentTypeRemote = "RemoteAttachment"
AttachmentTypeEmbedded = "EmbeddedAttachment"
AttachmentDataField = "uri"
)

var knownAttachmentTypes = []string{AttachmentTypeRemote, AttachmentTypeEmbedded} // nolint:gochecknoglobals

type AttachmentService struct {
httpClient httpClient
}

type httpClient interface {
Do(req *http.Request) (*http.Response, error)
}

func NewAttachmentService(
httpClient httpClient,
) *AttachmentService {
return &AttachmentService{
httpClient: httpClient,
}
}

func (s *AttachmentService) GetAttachments(
ctx context.Context,
subjects []verifiable.Subject,
) ([]map[string]interface{}, error) {
var allAttachments []*Attachment

for _, subject := range subjects {
allAttachments = append(allAttachments,
s.findAttachments(subject.CustomFields)...,
)
}

if len(allAttachments) == 0 {
return nil, nil
}

var final []map[string]interface{}

var wg sync.WaitGroup
for _, attachment := range allAttachments {
cloned := maphelpers.CopyMap(attachment.Claim)
attachment.Claim = cloned

final = append(final, attachment.Claim)

if attachment.Type == AttachmentTypeRemote {
wg.Add(1)
go func() {
defer wg.Done()

err := s.handleRemoteAttachment(ctx, cloned)
if err != nil {
cloned["error"] = fmt.Sprintf("failed to handle remote attachment: %s", err)
}
}()
}
}
wg.Wait()

return final, nil
}

func (s *AttachmentService) handleRemoteAttachment(
ctx context.Context,
attachment map[string]interface{},
) error {
targetURL := fmt.Sprint(attachment[AttachmentDataField])
if targetURL == "" {
return errors.New("url is required")

Check warning on line 97 in pkg/service/oidc4vp/attachments.go

View check run for this annotation

Codecov / codecov/patch

pkg/service/oidc4vp/attachments.go#L97

Added line #L97 was not covered by tests
}

httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, targetURL, nil)
if err != nil {
return fmt.Errorf("failed to create http request: %w", err)

Check warning on line 102 in pkg/service/oidc4vp/attachments.go

View check run for this annotation

Codecov / codecov/patch

pkg/service/oidc4vp/attachments.go#L102

Added line #L102 was not covered by tests
}

resp, err := s.httpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("failed to fetch url: %w", err)
}

var body []byte
if resp.Body != nil {
defer func() {
_ = resp.Body.Close() // nolint
}()

body, err = io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)

Check warning on line 118 in pkg/service/oidc4vp/attachments.go

View check run for this annotation

Codecov / codecov/patch

pkg/service/oidc4vp/attachments.go#L118

Added line #L118 was not covered by tests
}
}

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d and body %v", resp.StatusCode, string(body))
}

attachment[AttachmentDataField] = fmt.Sprintf("data:%s;base64,%s",
resp.Header.Get("Content-Type"),
base64.StdEncoding.EncodeToString(body),
)

return nil
}

// nolint:gocognit
func (s *AttachmentService) findAttachments(
targetMap map[string]interface{},
) []*Attachment {
var attachments []*Attachment

for k, v := range targetMap {
switch valTyped := v.(type) {
case []interface{}:
for _, item := range valTyped {
if nested, ok := item.(map[string]interface{}); ok {
attachments = append(attachments, s.findAttachments(nested)...)
}
}
case map[string]interface{}:
attachments = append(attachments, s.findAttachments(valTyped)...)
}

if k != "type" && k != "@type" {
continue
}

switch typed := v.(type) {
case string:
if lo.Contains(knownAttachmentTypes, typed) {
attachments = append(attachments, &Attachment{
Type: typed,
Claim: targetMap,
})
}
case []interface{}:
newSlice := make([]string, 0, len(typed))
for _, item := range typed {
newSlice = append(newSlice, fmt.Sprint(item))
}

for _, item := range newSlice {
if lo.Contains(knownAttachmentTypes, item) {
attachments = append(attachments, &Attachment{
Type: item,
Claim: targetMap,
})
}
}
case []string:
for _, item := range typed {
if lo.Contains(knownAttachmentTypes, item) {
attachments = append(attachments, &Attachment{
Type: item,
Claim: targetMap,
})
}
}
}
}

return attachments
}
Loading
Loading