Skip to content

Commit

Permalink
feat(sdk): issuer acknowledgment (#684)
Browse files Browse the repository at this point in the history
feat(sdk): issuer acknowledgment.

Signed-off-by: Volodymyr Kubiv <[email protected]>
  • Loading branch information
vkubiv authored Nov 28, 2023
1 parent 77e6e95 commit 2e48050
Show file tree
Hide file tree
Showing 11 changed files with 350 additions and 17 deletions.
16 changes: 16 additions & 0 deletions cmd/wallet-sdk-gomobile/openid4ci/issuerinitiatedinteraction.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,3 +266,19 @@ func (i *IssuerInitiatedInteraction) OTelTraceID() string {

return traceID
}

// RequireAcknowledgment if true indicates that the issuer requires to be acknowledged if
// the user accepts or rejects credentials.
func (i *IssuerInitiatedInteraction) RequireAcknowledgment() bool {
return i.goAPIInteraction.RequireAcknowledgment()
}

// AcknowledgeSuccess acknowledges the issuer that the client accepted credentials.
func (i *IssuerInitiatedInteraction) AcknowledgeSuccess() error {
return i.goAPIInteraction.AcknowledgeSuccess()
}

// AcknowledgeReject acknowledges the issuer that the client rejected credentials.
func (i *IssuerInitiatedInteraction) AcknowledgeReject() error {
return i.goAPIInteraction.AcknowledgeReject()
}
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ func (m *mockIssuerServerHandler) ServeHTTP(writer http.ResponseWriter, //nolint
default:
_, err = writer.Write(m.credentialResponse)
}
case "/ack_endpoint":
writer.WriteHeader(http.StatusNoContent)
}

require.NoError(m.t, err)
Expand Down Expand Up @@ -234,6 +236,9 @@ func TestIssuerInitiatedInteraction_RequestCredential(t *testing.T) {
t.Run("With TLS verification disabled", func(t *testing.T) {
doRequestCredentialTest(t, nil, true)
})
t.Run("Acknowledge reject", func(t *testing.T) {
doRequestCredentialTestExt(t, nil, false, true)
})
})
t.Run("Success with jwk public key", func(t *testing.T) {
issuerServerHandler := &mockIssuerServerHandler{
Expand Down Expand Up @@ -566,9 +571,16 @@ func TestIssuerInitiatedInteractionAlias(t *testing.T) {
require.NotNil(t, issuerMetadata)
}

//nolint:thelper // Not a test helper function
func doRequestCredentialTest(t *testing.T, additionalHeaders *api.Headers,
disableTLSVerification bool,
) {
t.Helper()
doRequestCredentialTestExt(t, additionalHeaders, disableTLSVerification, false)
}

//nolint:thelper // Not a test helper function
func doRequestCredentialTestExt(t *testing.T, additionalHeaders *api.Headers,
disableTLSVerification bool, acknowledgeReject bool,
) {
issuerServerHandler := &mockIssuerServerHandler{
t: t,
Expand All @@ -582,7 +594,8 @@ func doRequestCredentialTest(t *testing.T, additionalHeaders *api.Headers,
}

issuerServerHandler.issuerMetadata = fmt.Sprintf(`{"credential_endpoint":"%s/credential",`+
`"credential_issuer":"https://server.example.com"}`, server.URL)
`"credential_ack_endpoint":"%s/ack_endpoint",`+
`"credential_issuer":"https://server.example.com"}`, server.URL, server.URL)

defer server.Close()

Expand All @@ -609,6 +622,15 @@ func doRequestCredentialTest(t *testing.T, additionalHeaders *api.Headers,
}, "1234")
require.NoError(t, err)
require.NotNil(t, credentials)
require.True(t, interaction.RequireAcknowledgment())

if acknowledgeReject {
err = interaction.AcknowledgeReject()
} else {
err = interaction.AcknowledgeSuccess()
}

require.NoError(t, err)

numberOfActivitiesLogged := activityLogger.Length()
require.Equal(t, 1, numberOfActivitiesLogged)
Expand Down
1 change: 1 addition & 0 deletions pkg/models/issuer/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Metadata struct {
CredentialsSupported []SupportedCredential `json:"credentials_supported,omitempty"`
LocalizedIssuerDisplays []LocalizedIssuerDisplay `json:"display,omitempty"`
TokenEndpoint string `json:"token_endpoint,omitempty"`
CredentialAckEndpoint string `json:"credential_ack_endpoint"`
jwtKID *string
}

Expand Down
125 changes: 111 additions & 14 deletions pkg/openid4ci/interaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,37 @@ import (

const getIssuerMetadataEventText = "Get issuer metadata"

// AskStatus used to acknowledge issuer that client accepts or rejects credentials.
type AskStatus string

const (
// AskStatusSuccess acknowledge issuer that client accepts credentials.
AskStatusSuccess AskStatus = "success"
// AskStatusRejected acknowledge issuer that client rejects credentials.
AskStatusRejected AskStatus = "rejected"
)

// This is a common object shared by both the IssuerInitiatedInteraction and WalletInitiatedInteraction objects.
type interaction struct {
issuerURI string
clientID string
didResolver api.DIDResolver
activityLogger api.ActivityLogger
metricsLogger api.MetricsLogger
disableVCProofChecks bool
documentLoader ld.DocumentLoader
issuerMetadata *issuer.Metadata
openIDConfig *OpenIDConfig
oAuth2Config *oauth2.Config
authTokenResponse *oauth2.Token
httpClient *http.Client
authCodeURLState string
codeVerifier string
issuerURI string
clientID string
didResolver api.DIDResolver
activityLogger api.ActivityLogger
metricsLogger api.MetricsLogger
disableVCProofChecks bool
documentLoader ld.DocumentLoader
issuerMetadata *issuer.Metadata
openIDConfig *OpenIDConfig
oAuth2Config *oauth2.Config
authTokenResponse *oauth2.Token
httpClient *http.Client
authCodeURLState string
codeVerifier string
requestedAcknowledgment *requestedAcknowledgment
}

type requestedAcknowledgment struct {
askIDs []string
}

func (i *interaction) createAuthorizationURL(clientID, redirectURI, format string, types []string, issuerState *string,
Expand Down Expand Up @@ -413,6 +428,8 @@ func (i *interaction) getCredentialResponsesWithAuth(signer api.JWTSigner, crede
}

credentialResponses[index] = credentialResponse

i.storeAcknowledgmentID(credentialResponse.AscID)
}

return credentialResponses, nil
Expand Down Expand Up @@ -654,3 +671,83 @@ func (i *interaction) verifyIssuer() (string, error) {

return serviceURL, nil
}

func (i *interaction) requireAcknowledgment() bool {
return i.requestedAcknowledgment != nil && i.issuerMetadata.CredentialAckEndpoint != ""
}

func (i *interaction) storeAcknowledgmentID(id string) {
if i.requestedAcknowledgment == nil {
i.requestedAcknowledgment = &requestedAcknowledgment{}
}

i.requestedAcknowledgment.askIDs = append(i.requestedAcknowledgment.askIDs, id)
}

func (i *interaction) acknowledgeIssuer(status AskStatus, externalAccessToken string) error {
if !i.requireAcknowledgment() {
return fmt.Errorf("issuer not support credential acknowledgement")
}

var ackRequest acknowledgementRequest

for _, ackID := range i.requestedAcknowledgment.askIDs {
ackRequest.Credentials = append(ackRequest.Credentials, credentialAcknowledgement{
AckID: ackID,
Status: string(status),
IssuerIdentifier: i.issuerURI,
})
}

return i.sendAcknowledgeRequest(externalAccessToken, ackRequest)
}

func (i *interaction) sendAcknowledgeRequest(
externalAccessToken string, acknowledgementRequest acknowledgementRequest,
) error {
askEndpointURL := i.issuerMetadata.CredentialAckEndpoint

requestBytes, err := json.Marshal(acknowledgementRequest)
if err != nil {
return fmt.Errorf("fail to marshal acknowledgementRequest: %w", err)
}

req, err := http.NewRequestWithContext(context.Background(),
http.MethodPost, askEndpointURL, bytes.NewBuffer(requestBytes))
if err != nil {
return err
}

req.Header.Add("Content-Type", "application/json")

httpClient := i.httpClient

if externalAccessToken != "" {
req.Header.Add("Authorization", "Bearer "+externalAccessToken)
} else {
httpClient = i.createOAuthHTTPClient()
}

resp, err := httpClient.Do(req)
if err != nil {
return err
}

defer func() {
errClose := resp.Body.Close()
if errClose != nil {
println(fmt.Sprintf("failed to close response body: %s", errClose.Error()))
}
}()

respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return err
}

if resp.StatusCode != http.StatusNoContent {
return processCredentialErrorResponse(resp.StatusCode, respBytes)
}

return nil
}
32 changes: 32 additions & 0 deletions pkg/openid4ci/issuerinitiatedinteraction.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ type IssuerInitiatedInteraction struct {
credentialFormats []string
preAuthorizedCodeGrantParams *PreAuthorizedCodeGrantParams
authorizationCodeGrantParams *AuthorizationCodeGrantParams

preAuthTokenResponse *preAuthTokenResponse
}

// NewIssuerInitiatedInteraction creates a new OpenID4CI IssuerInitiatedInteraction.
Expand Down Expand Up @@ -265,6 +267,32 @@ func (i *IssuerInitiatedInteraction) VerifyIssuer() (string, error) {
return i.interaction.verifyIssuer()
}

// RequireAcknowledgment if true indicates that the issuer requires to be acknowledged if
// the user accepts or rejects credentials.
func (i *IssuerInitiatedInteraction) RequireAcknowledgment() bool {
return i.interaction.requireAcknowledgment()
}

// AcknowledgeSuccess acknowledge issuer that client accepts credentials.
func (i *IssuerInitiatedInteraction) AcknowledgeSuccess() error {
var exernalAccessToken string
if i.preAuthTokenResponse != nil {
exernalAccessToken = i.preAuthTokenResponse.AccessToken
}

return i.interaction.acknowledgeIssuer(AskStatusSuccess, exernalAccessToken)
}

// AcknowledgeReject acknowledge issuer that client rejects credentials.
func (i *IssuerInitiatedInteraction) AcknowledgeReject() error {
var exernalAccessToken string
if i.preAuthTokenResponse != nil {
exernalAccessToken = i.preAuthTokenResponse.AccessToken
}

return i.interaction.acknowledgeIssuer(AskStatusRejected, exernalAccessToken)
}

func (i *IssuerInitiatedInteraction) requestCredentialWithPreAuth(jwtSigner api.JWTSigner,
pin string,
) ([]*verifiable.Credential, error) {
Expand Down Expand Up @@ -362,8 +390,12 @@ func (i *IssuerInitiatedInteraction) getCredentialResponsesWithPreAuth(
}

credentialResponses[index] = credentialResponse

i.interaction.storeAcknowledgmentID(credentialResponse.AscID)
}

i.preAuthTokenResponse = tokenResponse

return credentialResponses, nil
}

Expand Down
Loading

0 comments on commit 2e48050

Please sign in to comment.