github.com/hyperledger/aries-framework-go@v0.3.2/pkg/wallet/didcomm.go (about)

     1  /*
     2  Copyright SecureKey Technologies Inc. All Rights Reserved.
     3  Copyright Avast Software. All Rights Reserved.
     4  
     5  SPDX-License-Identifier: Apache-2.0
     6  */
     7  
     8  package wallet
     9  
    10  import (
    11  	"context"
    12  	"fmt"
    13  	"time"
    14  
    15  	"github.com/google/uuid"
    16  
    17  	"github.com/hyperledger/aries-framework-go/pkg/client/didexchange"
    18  	"github.com/hyperledger/aries-framework-go/pkg/client/issuecredential"
    19  	"github.com/hyperledger/aries-framework-go/pkg/client/outofband"
    20  	"github.com/hyperledger/aries-framework-go/pkg/client/outofbandv2"
    21  	"github.com/hyperledger/aries-framework-go/pkg/client/presentproof"
    22  	"github.com/hyperledger/aries-framework-go/pkg/didcomm/common/model"
    23  	"github.com/hyperledger/aries-framework-go/pkg/didcomm/common/service"
    24  	"github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/decorator"
    25  	didexchangeSvc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/didexchange"
    26  	issuecredentialsvc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/issuecredential"
    27  	outofbandv2svc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/outofbandv2"
    28  	"github.com/hyperledger/aries-framework-go/pkg/kms"
    29  	"github.com/hyperledger/aries-framework-go/pkg/store/connection"
    30  	"github.com/hyperledger/aries-framework-go/spi/storage"
    31  )
    32  
    33  // miscellaneous constants.
    34  const (
    35  	msgEventBufferSize = 10
    36  	ldJSONMimeType     = "application/ld+json"
    37  
    38  	// protocol states.
    39  	stateNameAbandoned  = "abandoned"
    40  	stateNameAbandoning = "abandoning"
    41  	stateNameDone       = "done"
    42  
    43  	// timeout constants.
    44  	defaultDIDExchangeTimeOut                = 120 * time.Second
    45  	defaultWaitForRequestPresentationTimeOut = 120 * time.Second
    46  	defaultWaitForPresentProofDone           = 120 * time.Second
    47  	retryDelay                               = 500 * time.Millisecond
    48  )
    49  
    50  // didCommProvider to be used only if wallet needs to be participated in DIDComm operation.
    51  // TODO: using wallet KMS instead of provider KMS.
    52  // TODO: reconcile Protocol storage with wallet store.
    53  type didCommProvider interface {
    54  	KMS() kms.KeyManager
    55  	ServiceEndpoint() string
    56  	ProtocolStateStorageProvider() storage.Provider
    57  	Service(id string) (interface{}, error)
    58  	KeyType() kms.KeyType
    59  	KeyAgreementType() kms.KeyType
    60  }
    61  
    62  type combinedDidCommWalletProvider interface {
    63  	provider
    64  	didCommProvider
    65  }
    66  
    67  // DidComm enables access to verifiable credential wallet features.
    68  type DidComm struct {
    69  	// wallet implementation
    70  	wallet *Wallet
    71  
    72  	// present proof client
    73  	presentProofClient *presentproof.Client
    74  
    75  	// issue credential client
    76  	issueCredentialClient *issuecredential.Client
    77  
    78  	// out of band client
    79  	oobClient *outofband.Client
    80  
    81  	// out of band v2 client
    82  	oobV2Client *outofbandv2.Client
    83  
    84  	// did-exchange client
    85  	didexchangeClient *didexchange.Client
    86  
    87  	// connection lookup
    88  	connectionLookup *connection.Lookup
    89  }
    90  
    91  // NewDidComm returns new verifiable credential wallet for given user.
    92  // returns error if wallet profile is not found.
    93  // To create a new wallet profile, use `CreateProfile()`.
    94  // To update an existing profile, use `UpdateProfile()`.
    95  func NewDidComm(wallet *Wallet, ctx combinedDidCommWalletProvider) (*DidComm, error) {
    96  	presentProofClient, err := presentproof.New(ctx)
    97  	if err != nil {
    98  		return nil, fmt.Errorf("failed to initialize present proof client: %w", err)
    99  	}
   100  
   101  	issueCredentialClient, err := issuecredential.New(ctx)
   102  	if err != nil {
   103  		return nil, fmt.Errorf("failed to initialize issue credential client: %w", err)
   104  	}
   105  
   106  	oobClient, err := outofband.New(ctx)
   107  	if err != nil {
   108  		return nil, fmt.Errorf("failed to initialize out-of-band client: %w", err)
   109  	}
   110  
   111  	oobV2Client, err := outofbandv2.New(ctx)
   112  	if err != nil {
   113  		return nil, fmt.Errorf("failed to initialize out-of-band v2 client: %w", err)
   114  	}
   115  
   116  	connectionLookup, err := connection.NewLookup(ctx)
   117  	if err != nil {
   118  		return nil, fmt.Errorf("failed to initialize connection lookup: %w", err)
   119  	}
   120  
   121  	didexchangeClient, err := didexchange.New(ctx)
   122  	if err != nil {
   123  		return nil, fmt.Errorf("failed to initialize didexchange client: %w", err)
   124  	}
   125  
   126  	return &DidComm{
   127  		wallet:                wallet,
   128  		presentProofClient:    presentProofClient,
   129  		issueCredentialClient: issueCredentialClient,
   130  		oobClient:             oobClient,
   131  		oobV2Client:           oobV2Client,
   132  		didexchangeClient:     didexchangeClient,
   133  		connectionLookup:      connectionLookup,
   134  	}, nil
   135  }
   136  
   137  // Connect accepts out-of-band invitations and performs DID exchange.
   138  //
   139  // Args:
   140  // 		- authToken: authorization for performing create key pair operation.
   141  // 		- invitation: out-of-band invitation.
   142  // 		- options: connection options.
   143  //
   144  // Returns:
   145  // 		- connection ID if DID exchange is successful.
   146  // 		- error if operation false.
   147  //
   148  func (c *DidComm) Connect(authToken string, invitation *outofband.Invitation, options ...ConnectOptions) (string, error) { //nolint: lll
   149  	statusCh := make(chan service.StateMsg, msgEventBufferSize)
   150  
   151  	err := c.didexchangeClient.RegisterMsgEvent(statusCh)
   152  	if err != nil {
   153  		return "", fmt.Errorf("failed to register msg event : %w", err)
   154  	}
   155  
   156  	defer func() {
   157  		e := c.didexchangeClient.UnregisterMsgEvent(statusCh)
   158  		if e != nil {
   159  			logger.Warnf("Failed to unregister msg event for connect: %w", e)
   160  		}
   161  	}()
   162  
   163  	opts := &connectOpts{}
   164  	for _, opt := range options {
   165  		opt(opts)
   166  	}
   167  
   168  	connID, err := c.oobClient.AcceptInvitation(invitation, opts.Label, getOobMessageOptions(opts)...)
   169  	if err != nil {
   170  		return "", fmt.Errorf("failed to accept invitation : %w", err)
   171  	}
   172  
   173  	if opts.timeout == 0 {
   174  		opts.timeout = defaultDIDExchangeTimeOut
   175  	}
   176  
   177  	ctx, cancel := context.WithTimeout(context.Background(), opts.timeout)
   178  	defer cancel()
   179  
   180  	err = waitForConnect(ctx, statusCh, connID)
   181  	if err != nil {
   182  		return "", fmt.Errorf("wallet connect failed : %w", err)
   183  	}
   184  
   185  	return connID, nil
   186  }
   187  
   188  // ProposePresentation accepts out-of-band invitation and sends message proposing presentation
   189  // from wallet to relying party.
   190  // https://w3c-ccg.github.io/universal-wallet-interop-spec/#proposepresentation
   191  //
   192  // Currently Supporting
   193  // [0454-present-proof-v2](https://github.com/hyperledger/aries-rfcs/tree/master/features/0454-present-proof-v2)
   194  //
   195  // Args:
   196  // 		- authToken: authorization for performing operation.
   197  // 		- invitation: out-of-band invitation from relying party.
   198  // 		- options: options for accepting invitation and send propose presentation message.
   199  //
   200  // Returns:
   201  // 		- DIDCommMsgMap containing request presentation message if operation is successful.
   202  // 		- error if operation fails.
   203  //
   204  func (c *DidComm) ProposePresentation(authToken string, invitation *GenericInvitation, options ...InitiateInteractionOption) (*service.DIDCommMsgMap, error) { //nolint: lll
   205  	opts := &initiateInteractionOpts{}
   206  	for _, opt := range options {
   207  		opt(opts)
   208  	}
   209  
   210  	var (
   211  		connID string
   212  		err    error
   213  	)
   214  
   215  	switch invitation.Version() {
   216  	default:
   217  		fallthrough
   218  	case service.V1:
   219  		connID, err = c.Connect(authToken, (*outofband.Invitation)(invitation.AsV1()), opts.connectOpts...)
   220  		if err != nil {
   221  			return nil, fmt.Errorf("failed to perform did connection : %w", err)
   222  		}
   223  	case service.V2:
   224  		connOpts := &connectOpts{}
   225  
   226  		for _, opt := range opts.connectOpts {
   227  			opt(connOpts)
   228  		}
   229  
   230  		connID, err = c.oobV2Client.AcceptInvitation(
   231  			invitation.AsV2(),
   232  			outofbandv2svc.WithRouterConnections(connOpts.Connections),
   233  		)
   234  		if err != nil {
   235  			return nil, fmt.Errorf("failed to accept OOB v2 invitation : %w", err)
   236  		}
   237  	}
   238  
   239  	connRecord, err := c.connectionLookup.GetConnectionRecord(connID)
   240  	if err != nil {
   241  		return nil, fmt.Errorf("failed to lookup connection for propose presentation : %w", err)
   242  	}
   243  
   244  	opts = prepareInteractionOpts(connRecord, opts)
   245  
   246  	_, err = c.presentProofClient.SendProposePresentation(&presentproof.ProposePresentation{}, connRecord)
   247  	if err != nil {
   248  		return nil, fmt.Errorf("failed to propose presentation from wallet: %w", err)
   249  	}
   250  
   251  	ctx, cancel := context.WithTimeout(context.Background(), opts.timeout)
   252  	defer cancel()
   253  
   254  	return c.waitForRequestPresentation(ctx, connRecord)
   255  }
   256  
   257  // PresentProof sends message present proof message from wallet to relying party.
   258  // https://w3c-ccg.github.io/universal-wallet-interop-spec/#presentproof
   259  //
   260  // Currently Supporting
   261  // [0454-present-proof-v2](https://github.com/hyperledger/aries-rfcs/tree/master/features/0454-present-proof-v2)
   262  //
   263  // Args:
   264  // 		- authToken: authorization for performing operation.
   265  // 		- thID: thread ID (action ID) of request presentation.
   266  // 		- presentProofFrom: presentation to be sent.
   267  //
   268  // Returns:
   269  // 		- Credential interaction status containing status, redirectURL.
   270  // 		- error if operation fails.
   271  //
   272  func (c *DidComm) PresentProof(authToken, thID string, options ...ConcludeInteractionOptions) (*CredentialInteractionStatus, error) { //nolint: lll
   273  	opts := &concludeInteractionOpts{}
   274  
   275  	for _, option := range options {
   276  		option(opts)
   277  	}
   278  
   279  	var presentation interface{}
   280  	if opts.presentation != nil {
   281  		presentation = opts.presentation
   282  	} else {
   283  		presentation = opts.rawPresentation
   284  	}
   285  
   286  	err := c.presentProofClient.AcceptRequestPresentation(thID, &presentproof.Presentation{
   287  		Attachments: []decorator.GenericAttachment{{
   288  			ID: uuid.New().String(),
   289  			Data: decorator.AttachmentData{
   290  				JSON: presentation,
   291  			},
   292  		}},
   293  	}, nil)
   294  	if err != nil {
   295  		return nil, err
   296  	}
   297  
   298  	// wait for ack or problem-report.
   299  	if opts.waitForDone {
   300  		statusCh := make(chan service.StateMsg, msgEventBufferSize)
   301  
   302  		err = c.presentProofClient.RegisterMsgEvent(statusCh)
   303  		if err != nil {
   304  			return nil, fmt.Errorf("failed to register present proof msg event : %w", err)
   305  		}
   306  
   307  		defer func() {
   308  			e := c.presentProofClient.UnregisterMsgEvent(statusCh)
   309  			if e != nil {
   310  				logger.Warnf("Failed to unregister msg event for present proof: %w", e)
   311  			}
   312  		}()
   313  
   314  		ctx, cancel := context.WithTimeout(context.Background(), opts.timeout)
   315  		defer cancel()
   316  
   317  		return waitCredInteractionCompletion(ctx, statusCh, thID)
   318  	}
   319  
   320  	return &CredentialInteractionStatus{Status: model.AckStatusPENDING}, nil
   321  }
   322  
   323  // ProposeCredential sends propose credential message from wallet to issuer.
   324  // https://w3c-ccg.github.io/universal-wallet-interop-spec/#proposecredential
   325  //
   326  // Currently Supporting : 0453-issueCredentialV2
   327  // https://github.com/hyperledger/aries-rfcs/blob/main/features/0453-issue-credential-v2/README.md
   328  //
   329  // Args:
   330  // 		- authToken: authorization for performing operation.
   331  // 		- invitation: out-of-band invitation from issuer.
   332  // 		- options: options for accepting invitation and send propose credential message.
   333  //
   334  // Returns:
   335  // 		- DIDCommMsgMap containing offer credential message if operation is successful.
   336  // 		- error if operation fails.
   337  //
   338  func (c *DidComm) ProposeCredential(authToken string, invitation *GenericInvitation, options ...InitiateInteractionOption) (*service.DIDCommMsgMap, error) { //nolint: lll
   339  	opts := &initiateInteractionOpts{}
   340  	for _, opt := range options {
   341  		opt(opts)
   342  	}
   343  
   344  	var (
   345  		connID string
   346  		err    error
   347  	)
   348  
   349  	switch invitation.Version() {
   350  	default:
   351  		fallthrough
   352  	case service.V1:
   353  		connID, err = c.Connect(authToken, (*outofband.Invitation)(invitation.AsV1()), opts.connectOpts...)
   354  		if err != nil {
   355  			return nil, fmt.Errorf("failed to perform did connection : %w", err)
   356  		}
   357  	case service.V2:
   358  		connOpts := &connectOpts{}
   359  
   360  		for _, opt := range opts.connectOpts {
   361  			opt(connOpts)
   362  		}
   363  
   364  		connID, err = c.oobV2Client.AcceptInvitation(
   365  			invitation.AsV2(),
   366  			outofbandv2svc.WithRouterConnections(connOpts.Connections),
   367  		)
   368  		if err != nil {
   369  			return nil, fmt.Errorf("failed to accept OOB v2 invitation : %w", err)
   370  		}
   371  	}
   372  
   373  	connRecord, err := c.connectionLookup.GetConnectionRecord(connID)
   374  	if err != nil {
   375  		return nil, fmt.Errorf("failed to lookup connection for propose presentation : %w", err)
   376  	}
   377  
   378  	opts = prepareInteractionOpts(connRecord, opts)
   379  
   380  	_, err = c.issueCredentialClient.SendProposal(
   381  		&issuecredential.ProposeCredential{InvitationID: invitation.ID},
   382  		connRecord,
   383  	)
   384  	if err != nil {
   385  		return nil, fmt.Errorf("failed to propose credential from wallet: %w", err)
   386  	}
   387  
   388  	ctx, cancel := context.WithTimeout(context.Background(), opts.timeout)
   389  	defer cancel()
   390  
   391  	return c.waitForOfferCredential(ctx, connRecord)
   392  }
   393  
   394  // RequestCredential sends request credential message from wallet to issuer and
   395  // optionally waits for credential response.
   396  // https://w3c-ccg.github.io/universal-wallet-interop-spec/#requestcredential
   397  //
   398  // Currently Supporting : 0453-issueCredentialV2
   399  // https://github.com/hyperledger/aries-rfcs/blob/main/features/0453-issue-credential-v2/README.md
   400  //
   401  // Args:
   402  // 		- authToken: authorization for performing operation.
   403  // 		- thID: thread ID (action ID) of offer credential message previously received.
   404  // 		- concludeInteractionOptions: options to conclude interaction like presentation to be shared etc.
   405  //
   406  // Returns:
   407  // 		- Credential interaction status containing status, redirectURL.
   408  // 		- error if operation fails.
   409  //
   410  func (c *DidComm) RequestCredential(authToken, thID string, options ...ConcludeInteractionOptions) (*CredentialInteractionStatus, error) { //nolint: lll
   411  	opts := &concludeInteractionOpts{}
   412  
   413  	for _, option := range options {
   414  		option(opts)
   415  	}
   416  
   417  	var presentation interface{}
   418  	if opts.presentation != nil {
   419  		presentation = opts.presentation
   420  	} else {
   421  		presentation = opts.rawPresentation
   422  	}
   423  
   424  	attachmentID := uuid.New().String()
   425  
   426  	err := c.issueCredentialClient.AcceptOffer(thID, &issuecredential.RequestCredential{
   427  		Type: issuecredentialsvc.RequestCredentialMsgTypeV2,
   428  		Formats: []issuecredentialsvc.Format{{
   429  			AttachID: attachmentID,
   430  			Format:   ldJSONMimeType,
   431  		}},
   432  		Attachments: []decorator.GenericAttachment{{
   433  			ID: attachmentID,
   434  			Data: decorator.AttachmentData{
   435  				JSON: presentation,
   436  			},
   437  		}},
   438  	})
   439  	if err != nil {
   440  		return nil, err
   441  	}
   442  
   443  	// wait for credential response.
   444  	if opts.waitForDone {
   445  		statusCh := make(chan service.StateMsg, msgEventBufferSize)
   446  
   447  		err = c.issueCredentialClient.RegisterMsgEvent(statusCh)
   448  		if err != nil {
   449  			return nil, fmt.Errorf("failed to register issue credential action event : %w", err)
   450  		}
   451  
   452  		defer func() {
   453  			e := c.issueCredentialClient.UnregisterMsgEvent(statusCh)
   454  			if e != nil {
   455  				logger.Warnf("Failed to unregister action event for issue credential: %w", e)
   456  			}
   457  		}()
   458  
   459  		ctx, cancel := context.WithTimeout(context.Background(), opts.timeout)
   460  		defer cancel()
   461  
   462  		return waitCredInteractionCompletion(ctx, statusCh, thID)
   463  	}
   464  
   465  	return &CredentialInteractionStatus{Status: model.AckStatusPENDING}, nil
   466  }
   467  
   468  // currently correlating response action by connection due to limitation in current present proof V1 implementation.
   469  func (c *DidComm) waitForRequestPresentation(ctx context.Context, record *connection.Record) (*service.DIDCommMsgMap, error) { //nolint: lll
   470  	done := make(chan *service.DIDCommMsgMap)
   471  
   472  	go func() {
   473  		for {
   474  			actions, err := c.presentProofClient.Actions()
   475  			if err != nil {
   476  				continue
   477  			}
   478  
   479  			if len(actions) > 0 {
   480  				for _, action := range actions {
   481  					if action.MyDID == record.MyDID && action.TheirDID == record.TheirDID {
   482  						done <- &action.Msg
   483  						return
   484  					}
   485  				}
   486  			}
   487  
   488  			select {
   489  			default:
   490  				time.Sleep(retryDelay)
   491  			case <-ctx.Done():
   492  				return
   493  			}
   494  		}
   495  	}()
   496  
   497  	select {
   498  	case msg := <-done:
   499  		return msg, nil
   500  	case <-ctx.Done():
   501  		return nil, fmt.Errorf("timeout waiting for request presentation message")
   502  	}
   503  }
   504  
   505  // currently correlating response action by connection due to limitation in current issue credential V1 implementation.
   506  func (c *DidComm) waitForOfferCredential(ctx context.Context, record *connection.Record) (*service.DIDCommMsgMap, error) { //nolint: lll
   507  	done := make(chan *service.DIDCommMsgMap)
   508  
   509  	go func() {
   510  		for {
   511  			actions, err := c.issueCredentialClient.Actions()
   512  			if err != nil {
   513  				continue
   514  			}
   515  
   516  			if len(actions) > 0 {
   517  				for _, action := range actions {
   518  					if action.MyDID == record.MyDID && action.TheirDID == record.TheirDID {
   519  						done <- &action.Msg
   520  						return
   521  					}
   522  				}
   523  			}
   524  
   525  			select {
   526  			default:
   527  				time.Sleep(retryDelay)
   528  			case <-ctx.Done():
   529  				return
   530  			}
   531  		}
   532  	}()
   533  
   534  	select {
   535  	case msg := <-done:
   536  		return msg, nil
   537  	case <-ctx.Done():
   538  		return nil, fmt.Errorf("timeout waiting for offer credential message")
   539  	}
   540  }
   541  
   542  func waitForConnect(ctx context.Context, didStateMsgs chan service.StateMsg, connID string) error {
   543  	done := make(chan struct{})
   544  
   545  	go func() {
   546  		for msg := range didStateMsgs {
   547  			if msg.Type != service.PostState || msg.StateID != didexchangeSvc.StateIDCompleted {
   548  				continue
   549  			}
   550  
   551  			var event model.Event
   552  
   553  			switch p := msg.Properties.(type) {
   554  			case model.Event:
   555  				event = p
   556  			default:
   557  				logger.Warnf("failed to cast didexchange event properties")
   558  
   559  				continue
   560  			}
   561  
   562  			if event.ConnectionID() == connID {
   563  				logger.Debugf(
   564  					"Received connection complete event for invitationID=%s connectionID=%s",
   565  					event.InvitationID(), event.ConnectionID())
   566  
   567  				close(done)
   568  
   569  				break
   570  			}
   571  		}
   572  	}()
   573  
   574  	select {
   575  	case <-done:
   576  		return nil
   577  	case <-ctx.Done():
   578  		return fmt.Errorf("time out waiting for did exchange state 'completed'")
   579  	}
   580  }
   581  
   582  // wait for credential interaction to be completed (done or abandoned protocol state).
   583  func waitCredInteractionCompletion(ctx context.Context, didStateMsgs chan service.StateMsg, thID string) (*CredentialInteractionStatus, error) { // nolint:gocognit,gocyclo,lll
   584  	done := make(chan *CredentialInteractionStatus)
   585  
   586  	go func() {
   587  		for msg := range didStateMsgs {
   588  			// match post state.
   589  			if msg.Type != service.PostState {
   590  				continue
   591  			}
   592  
   593  			// invalid state msg.
   594  			if msg.Msg == nil {
   595  				continue
   596  			}
   597  
   598  			msgThID, err := msg.Msg.ThreadID()
   599  			if err != nil {
   600  				continue
   601  			}
   602  
   603  			// match parent thread ID.
   604  			if msg.Msg.ParentThreadID() != thID && msgThID != thID {
   605  				continue
   606  			}
   607  
   608  			// match protocol state.
   609  			if msg.StateID != stateNameDone && msg.StateID != stateNameAbandoned && msg.StateID != stateNameAbandoning {
   610  				continue
   611  			}
   612  
   613  			properties := msg.Properties.All()
   614  
   615  			response := &CredentialInteractionStatus{}
   616  			response.RedirectURL, response.Status = getWebRedirectInfo(properties)
   617  
   618  			// if redirect status missing, then use protocol state, done -> OK, abandoned -> FAIL.
   619  			if response.Status == "" {
   620  				if msg.StateID == stateNameAbandoned || msg.StateID == stateNameAbandoning {
   621  					response.Status = model.AckStatusFAIL
   622  				} else {
   623  					response.Status = model.AckStatusOK
   624  				}
   625  			}
   626  
   627  			done <- response
   628  
   629  			return
   630  		}
   631  	}()
   632  
   633  	select {
   634  	case status := <-done:
   635  		return status, nil
   636  	case <-ctx.Done():
   637  		return nil, fmt.Errorf("time out waiting for credential interaction to get completed")
   638  	}
   639  }
   640  
   641  func prepareInteractionOpts(connRecord *connection.Record, opts *initiateInteractionOpts) *initiateInteractionOpts {
   642  	if opts.from == "" {
   643  		opts.from = connRecord.TheirDID
   644  	}
   645  
   646  	if opts.timeout == 0 {
   647  		opts.timeout = defaultWaitForRequestPresentationTimeOut
   648  	}
   649  
   650  	return opts
   651  }
   652  
   653  // getWebRedirectInfo reads web redirect info from properties.
   654  func getWebRedirectInfo(properties map[string]interface{}) (string, string) {
   655  	var redirect, status string
   656  
   657  	if redirectURL, ok := properties[webRedirectURLKey]; ok {
   658  		redirect = redirectURL.(string) //nolint: errcheck, forcetypeassert
   659  	}
   660  
   661  	if redirectStatus, ok := properties[webRedirectStatusKey]; ok {
   662  		status = redirectStatus.(string) //nolint: errcheck, forcetypeassert
   663  	}
   664  
   665  	return redirect, status
   666  }