diff --git a/pkg/models/issuer/metadata.go b/pkg/models/issuer/metadata.go index a9978f93..935ddfe1 100644 --- a/pkg/models/issuer/metadata.go +++ b/pkg/models/issuer/metadata.go @@ -14,6 +14,7 @@ type Metadata struct { CredentialEndpoint string `json:"credential_endpoint,omitempty"` CredentialsSupported []SupportedCredential `json:"credentials_supported,omitempty"` LocalizedIssuerDisplays []LocalizedIssuerDisplay `json:"display,omitempty"` + TokenEndpoint string `json:"token_endpoint,omitempty"` } // SupportedCredential represents metadata about a credential type that a credential issuer can issue. diff --git a/pkg/openid4ci/errors.go b/pkg/openid4ci/errors.go index c995eaef..760d2383 100644 --- a/pkg/openid4ci/errors.go +++ b/pkg/openid4ci/errors.go @@ -30,6 +30,7 @@ const ( UnsupportedCredentialTypeError = "UNSUPPORTED_CREDENTIAL_TYPE" InvalidOrMissingProofError = "INVALID_OR_MISSING_PROOF" UnsupportedIssuanceURISchemeError = "UNSUPPORTED_ISSUANCE_URI_SCHEME" + NoTokenEndpointAvailableError = "NO_TOKEN_ENDPOINT_AVAILABLE" //nolint:gosec //false positive ) // Constants' names and reasons are obvious, so they do not require additional comments. @@ -55,4 +56,5 @@ const ( UnsupportedCredentialTypeErrorCode = 17 InvalidOrMissingProofErrorCode = 18 UnsupportedIssuanceURISchemeCode + NoTokenEndpointAvailableErrorCode = 19 ) diff --git a/pkg/openid4ci/interaction.go b/pkg/openid4ci/interaction.go index 02d9d01b..cec4dea3 100644 --- a/pkg/openid4ci/interaction.go +++ b/pkg/openid4ci/interaction.go @@ -35,6 +35,8 @@ import ( "github.com/trustbloc/wallet-sdk/pkg/walleterror" ) +const getIssuerMetadataEventText = "Get issuer metadata" + // This is a common object shared by both the IssuerInitiatedInteraction and WalletInitiatedInteraction objects. type interaction struct { issuerURI string @@ -57,7 +59,7 @@ type interaction struct { func (i *interaction) createAuthorizationURL(clientID, redirectURI, format string, types []string, issuerState *string, scopes []string, useOAuthDiscoverableClientIDScheme bool, ) (string, error) { - err := i.populateIssuerMetadata() + err := i.populateIssuerMetadata("Authorization") if err != nil { return "", err } @@ -191,16 +193,12 @@ func (i *interaction) requestAccessToken(redirectURIWithAuthCode string) error { errors.New("state in redirect URI does not match the state from the authorization URL")) } - i.openIDConfig, err = i.getOpenIDConfig() + tokenEndpoint, err := i.getTokenEndpoint() if err != nil { - return walleterror.NewExecutionError( - ErrorModule, - IssuerOpenIDConfigFetchFailedCode, - IssuerOpenIDConfigFetchFailedError, - fmt.Errorf("failed to fetch issuer's OpenID configuration: %w", err)) + return err } - i.oAuth2Config.Endpoint.TokenURL = i.openIDConfig.TokenEndpoint + i.oAuth2Config.Endpoint.TokenURL = tokenEndpoint ctx := context.WithValue(context.Background(), oauth2.HTTPClient, i.httpClient) @@ -210,6 +208,41 @@ func (i *interaction) requestAccessToken(redirectURIWithAuthCode string) error { return err } +func (i *interaction) getTokenEndpoint() (string, error) { + var err error + + i.openIDConfig, err = i.getOpenIDConfig() + if err != nil { + // Fall back to the issuer metadata. See if it defines the token endpoint instead. + if i.issuerMetadata.TokenEndpoint == "" { + return "", walleterror.NewExecutionError( + ErrorModule, + NoTokenEndpointAvailableErrorCode, + NoTokenEndpointAvailableError, + fmt.Errorf("no token endpoint available. An OpenID configuration couldn't be fetched, and "+ + "the issuer's metadata doesn't specify a token endpoint. "+ + "OpenID configuration fetch error: %w", err)) + } + + return i.issuerMetadata.TokenEndpoint, nil + } + + if i.openIDConfig.TokenEndpoint != "" { + return i.openIDConfig.TokenEndpoint, nil + } + + if i.issuerMetadata.TokenEndpoint != "" { + return i.issuerMetadata.TokenEndpoint, nil + } + + return "", walleterror.NewExecutionError( + ErrorModule, + NoTokenEndpointAvailableErrorCode, + NoTokenEndpointAvailableError, + errors.New("no token endpoint available. Neither the OpenID configuration nor the issuer's "+ + "metadata specify one")) +} + func (i *interaction) dynamicClientRegistrationSupported() (bool, error) { var err error @@ -272,33 +305,23 @@ func (i *interaction) getOpenIDConfig() (*OpenIDConfig, error) { return &config, nil } -// getIssuerMetadata returns the issuer's metadata. If the issuer's metadata has already been fetched before, -// then it's returned without making an additional call. -func (i *interaction) getIssuerMetadata() (*issuer.Metadata, error) { +// If the issuer's metadata has not been fetched before in this interaction's lifespan, then this method fetches the +// issuer's metadata and stores it within this interaction object. If the issuer's metadata has already been fetched +// before, then this method does nothing in order to avoid making an unnecessary GET call. +func (i *interaction) populateIssuerMetadata(parentEvent string) error { if i.issuerMetadata == nil { - err := i.populateIssuerMetadata() + issuerMetadata, err := metadatafetcher.Get(i.issuerURI, i.httpClient, i.metricsLogger, parentEvent) if err != nil { - return nil, err + return walleterror.NewExecutionError( + ErrorModule, + MetadataFetchFailedCode, + MetadataFetchFailedError, + fmt.Errorf("failed to get issuer metadata: %w", err)) } - } - - return i.issuerMetadata, nil -} -// populateIssuerMetadata fetches the issuer's metadata and stores it within this interaction object. -func (i *interaction) populateIssuerMetadata() error { - issuerMetadata, err := metadatafetcher.Get(i.issuerURI, i.httpClient, i.metricsLogger, - "Authorization") - if err != nil { - return walleterror.NewExecutionError( - ErrorModule, - MetadataFetchFailedCode, - MetadataFetchFailedError, - fmt.Errorf("failed to get issuer metadata: %w", err)) + i.issuerMetadata = issuerMetadata } - i.issuerMetadata = issuerMetadata - return nil } diff --git a/pkg/openid4ci/issuerinitiatedinteraction.go b/pkg/openid4ci/issuerinitiatedinteraction.go index cb4dc762..59e536b9 100644 --- a/pkg/openid4ci/issuerinitiatedinteraction.go +++ b/pkg/openid4ci/issuerinitiatedinteraction.go @@ -26,7 +26,6 @@ import ( "github.com/trustbloc/wallet-sdk/pkg/api" "github.com/trustbloc/wallet-sdk/pkg/internal/httprequest" - metadatafetcher "github.com/trustbloc/wallet-sdk/pkg/internal/issuermetadata" "github.com/trustbloc/wallet-sdk/pkg/walleterror" ) @@ -252,7 +251,12 @@ func (i *IssuerInitiatedInteraction) DynamicClientRegistrationEndpoint() (string // IssuerMetadata returns the issuer's metadata. func (i *IssuerInitiatedInteraction) IssuerMetadata() (*issuer.Metadata, error) { - return i.interaction.getIssuerMetadata() + err := i.interaction.populateIssuerMetadata(getIssuerMetadataEventText) + if err != nil { + return nil, err + } + + return i.interaction.issuerMetadata, nil } func (i *IssuerInitiatedInteraction) requestCredentialWithPreAuth(jwtSigner api.JWTSigner, @@ -304,21 +308,20 @@ func (i *IssuerInitiatedInteraction) requestCredentialWithPreAuth(jwtSigner api. }) } -func (i *IssuerInitiatedInteraction) getCredentialResponsesWithPreAuth( //nolint:funlen // Difficult to decompose +func (i *IssuerInitiatedInteraction) getCredentialResponsesWithPreAuth( pin string, signer api.JWTSigner, ) ([]CredentialResponse, error) { - var err error + err := i.interaction.populateIssuerMetadata(requestCredentialEventText) + if err != nil { + return nil, err + } - i.interaction.openIDConfig, err = i.interaction.getOpenIDConfig() + tokenEndpoint, err := i.interaction.getTokenEndpoint() if err != nil { - return nil, walleterror.NewExecutionError( - ErrorModule, - IssuerOpenIDConfigFetchFailedCode, - IssuerOpenIDConfigFetchFailedError, - fmt.Errorf("failed to fetch issuer's OpenID configuration: %w", err)) + return nil, err } - tokenResponse, err := i.getPreAuthTokenResponse(pin) + tokenResponse, err := i.getPreAuthTokenResponse(pin, tokenEndpoint) if err != nil { return nil, fmt.Errorf("failed to get token response: %w", err) } @@ -328,17 +331,6 @@ func (i *IssuerInitiatedInteraction) getCredentialResponsesWithPreAuth( //nolint return nil, err } - i.interaction.issuerMetadata, err = metadatafetcher.Get(i.interaction.issuerURI, i.interaction.httpClient, - i.interaction.metricsLogger, - requestCredentialEventText) - if err != nil { - return nil, walleterror.NewExecutionError( - ErrorModule, - MetadataFetchFailedCode, - MetadataFetchFailedError, - fmt.Errorf("failed to get issuer metadata: %w", err)) - } - credentialResponses := make([]CredentialResponse, len(i.credentialTypes)) for index := range i.credentialTypes { @@ -372,7 +364,7 @@ func (i *IssuerInitiatedInteraction) getCredentialResponsesWithPreAuth( //nolint return credentialResponses, nil } -func (i *IssuerInitiatedInteraction) getPreAuthTokenResponse(pin string) (*preAuthTokenResponse, error) { +func (i *IssuerInitiatedInteraction) getPreAuthTokenResponse(pin, tokenEndpoint string) (*preAuthTokenResponse, error) { params := url.Values{} params.Add("grant_type", preAuthorizedGrantType) params.Add("pre-authorized_code", i.preAuthorizedCodeGrantParams.preAuthorizedCode) @@ -384,8 +376,8 @@ func (i *IssuerInitiatedInteraction) getPreAuthTokenResponse(pin string) (*preAu paramsReader := strings.NewReader(params.Encode()) responseBytes, err := httprequest.New(i.interaction.httpClient, i.interaction.metricsLogger).Do( - http.MethodPost, i.interaction.openIDConfig.TokenEndpoint, "application/x-www-form-urlencoded", paramsReader, - fmt.Sprintf(fetchTokenViaPOSTReqEventText, i.interaction.openIDConfig.TokenEndpoint), + http.MethodPost, tokenEndpoint, "application/x-www-form-urlencoded", paramsReader, + fmt.Sprintf(fetchTokenViaPOSTReqEventText, tokenEndpoint), requestCredentialEventText, tokenErrorResponseHandler) if err != nil { return nil, fmt.Errorf("issuer's token endpoint: %w", err) diff --git a/pkg/openid4ci/issuerinitiatedinteraction_test.go b/pkg/openid4ci/issuerinitiatedinteraction_test.go index 104402e0..6d8c4792 100644 --- a/pkg/openid4ci/issuerinitiatedinteraction_test.go +++ b/pkg/openid4ci/issuerinitiatedinteraction_test.go @@ -150,7 +150,7 @@ func (f *failingMetricsLogger) Log(metricsEvent *api.MetricsEvent) error { return nil } -func TestNewInteraction(t *testing.T) { +func TestNewIssuerInitiatedInteraction(t *testing.T) { t.Run("Success", func(t *testing.T) { t.Run("Credential format is jwt_vc_json", func(t *testing.T) { newIssuerInitiatedInteraction(t, createCredentialOfferIssuanceURI(t, "example.com", false, true)) @@ -398,29 +398,53 @@ func TestIssuerInitiatedInteraction_RequestCredential(t *testing.T) { t.Run("Pre-auth flow", func(t *testing.T) { t.Run("Success", func(t *testing.T) { t.Run("Using credential_offer", func(t *testing.T) { - issuerServerHandler := &mockIssuerServerHandler{ - t: t, - credentialResponse: sampleCredentialResponse, - } - - server := httptest.NewServer(issuerServerHandler) - defer server.Close() - - issuerServerHandler.openIDConfig = &openid4ci.OpenIDConfig{ - TokenEndpoint: fmt.Sprintf("%s/oidc/token", server.URL), - } - - issuerServerHandler.issuerMetadata = fmt.Sprintf(`{"credential_endpoint":"%s/credential"}`, - server.URL) - - interaction := newIssuerInitiatedInteraction(t, createCredentialOfferIssuanceURI(t, server.URL, false, true)) - - credentials, err := interaction.RequestCredentialWithPreAuth(&jwtSignerMock{ - keyID: mockKeyID, - }, openid4ci.WithPIN("1234")) - require.NoError(t, err) - require.Len(t, credentials, 1) - require.NotEmpty(t, credentials[0]) + t.Run("Token endpoint defined in the OpenID configuration", func(t *testing.T) { + issuerServerHandler := &mockIssuerServerHandler{ + t: t, + credentialResponse: sampleCredentialResponse, + } + + server := httptest.NewServer(issuerServerHandler) + defer server.Close() + + issuerServerHandler.openIDConfig = &openid4ci.OpenIDConfig{ + TokenEndpoint: fmt.Sprintf("%s/oidc/token", server.URL), + } + + issuerServerHandler.issuerMetadata = fmt.Sprintf(`{"credential_endpoint":"%s/credential"}`, + server.URL) + + interaction := newIssuerInitiatedInteraction(t, createCredentialOfferIssuanceURI(t, server.URL, false, true)) + + credentials, err := interaction.RequestCredentialWithPreAuth(&jwtSignerMock{ + keyID: mockKeyID, + }, openid4ci.WithPIN("1234")) + require.NoError(t, err) + require.Len(t, credentials, 1) + require.NotEmpty(t, credentials[0]) + }) + t.Run("Token endpoint defined in the issuer's metadata", func(t *testing.T) { + issuerServerHandler := &mockIssuerServerHandler{ + t: t, + credentialResponse: sampleCredentialResponse, + openIDConfig: &openid4ci.OpenIDConfig{}, + } + + server := httptest.NewServer(issuerServerHandler) + defer server.Close() + + issuerServerHandler.issuerMetadata = fmt.Sprintf(`{"credential_endpoint":"%s/credential",`+ + `"token_endpoint":"%s/oidc/token"}`, server.URL, server.URL) + + interaction := newIssuerInitiatedInteraction(t, createCredentialOfferIssuanceURI(t, server.URL, false, true)) + + credentials, err := interaction.RequestCredentialWithPreAuth(&jwtSignerMock{ + keyID: mockKeyID, + }, openid4ci.WithPIN("1234")) + require.NoError(t, err) + require.Len(t, credentials, 1) + require.NotEmpty(t, credentials[0]) + }) }) t.Run("Using credential_offer_uri", func(t *testing.T) { issuerServerHandler := &mockIssuerServerHandler{ @@ -485,23 +509,33 @@ func TestIssuerInitiatedInteraction_RequestCredential(t *testing.T) { "the credential offer requires a user PIN, but none was provided") require.Nil(t, credentials) }) - t.Run("Fail to fetch issuer's OpenID configuration", func(t *testing.T) { - requestURI := createCredentialOfferIssuanceURI(t, "BadURL", false, true) + t.Run("No token endpoint available - neither the OpenID configuration nor the issuer's metadata "+ + "specify one", func(t *testing.T) { + issuerServerHandler := &mockIssuerServerHandler{ + t: t, + issuerMetadata: "{}", + } + + server := httptest.NewServer(issuerServerHandler) + defer server.Close() + + requestURI := createCredentialOfferIssuanceURI(t, server.URL, false, true) interaction := newIssuerInitiatedInteraction(t, requestURI) credentials, err := interaction.RequestCredentialWithPreAuth(&jwtSignerMock{ keyID: mockKeyID, }, openid4ci.WithPIN("1234")) - require.Contains(t, err.Error(), "ISSUER_OPENID_CONFIG_FETCH_FAILED(OCI1-0003):failed to fetch issuer's "+ - `OpenID configuration: openid configuration endpoint: `+ - `Get "BadURL/.well-known/openid-configuration": unsupported protocol scheme ""`) + require.EqualError(t, err, "failed to get credential response: "+ + "NO_TOKEN_ENDPOINT_AVAILABLE(OCI1-0019):no token endpoint available. Neither the OpenID "+ + "configuration nor the issuer's metadata specify one") require.Nil(t, credentials) }) t.Run("Fail to reach issuer token endpoint", func(t *testing.T) { issuerServerHandler := &mockIssuerServerHandler{ - t: t, - openIDConfig: &openid4ci.OpenIDConfig{TokenEndpoint: "http://BadURL"}, + t: t, + openIDConfig: &openid4ci.OpenIDConfig{TokenEndpoint: "http://BadURL"}, + issuerMetadata: "{}", } server := httptest.NewServer(issuerServerHandler) @@ -518,7 +552,11 @@ func TestIssuerInitiatedInteraction_RequestCredential(t *testing.T) { }) t.Run("Fail to get token response: server response body is not an errorResponse "+ "object", func(t *testing.T) { - issuerServerHandler := &mockIssuerServerHandler{t: t, tokenRequestShouldFail: true} + issuerServerHandler := &mockIssuerServerHandler{ + t: t, + tokenRequestShouldFail: true, + issuerMetadata: "{}", + } server := httptest.NewServer(issuerServerHandler) defer server.Close() @@ -540,8 +578,10 @@ func TestIssuerInitiatedInteraction_RequestCredential(t *testing.T) { }) t.Run("Fail to get token response: invalid token request", func(t *testing.T) { issuerServerHandler := &mockIssuerServerHandler{ - t: t, tokenRequestShouldFail: true, + t: t, + tokenRequestShouldFail: true, tokenRequestErrorResponse: `{"error":"invalid_request"}`, + issuerMetadata: "{}", } server := httptest.NewServer(issuerServerHandler) defer server.Close() @@ -564,8 +604,10 @@ func TestIssuerInitiatedInteraction_RequestCredential(t *testing.T) { }) t.Run("Fail to get token response: invalid grant", func(t *testing.T) { issuerServerHandler := &mockIssuerServerHandler{ - t: t, tokenRequestShouldFail: true, + t: t, + tokenRequestShouldFail: true, tokenRequestErrorResponse: `{"error":"invalid_grant"}`, + issuerMetadata: "{}", } server := httptest.NewServer(issuerServerHandler) defer server.Close() @@ -588,8 +630,10 @@ func TestIssuerInitiatedInteraction_RequestCredential(t *testing.T) { }) t.Run("Fail to get token response: invalid client", func(t *testing.T) { issuerServerHandler := &mockIssuerServerHandler{ - t: t, tokenRequestShouldFail: true, + t: t, + tokenRequestShouldFail: true, tokenRequestErrorResponse: `{"error":"invalid_client"}`, + issuerMetadata: "{}", } server := httptest.NewServer(issuerServerHandler) defer server.Close() @@ -612,8 +656,10 @@ func TestIssuerInitiatedInteraction_RequestCredential(t *testing.T) { }) t.Run("Fail to get token response: other error code", func(t *testing.T) { issuerServerHandler := &mockIssuerServerHandler{ - t: t, tokenRequestShouldFail: true, + t: t, + tokenRequestShouldFail: true, tokenRequestErrorResponse: `{"error":"someOtherErrorCode"}`, + issuerMetadata: "{}", } server := httptest.NewServer(issuerServerHandler) defer server.Close() @@ -635,7 +681,11 @@ func TestIssuerInitiatedInteraction_RequestCredential(t *testing.T) { require.Nil(t, credentials) }) t.Run("Fail to unmarshal response from issuer token endpoint", func(t *testing.T) { - issuerServerHandler := &mockIssuerServerHandler{t: t, tokenRequestShouldGiveUnmarshallableResponse: true} + issuerServerHandler := &mockIssuerServerHandler{ + t: t, + tokenRequestShouldGiveUnmarshallableResponse: true, + issuerMetadata: "{}", + } server := httptest.NewServer(issuerServerHandler) defer server.Close() @@ -984,7 +1034,8 @@ func TestIssuerInitiatedInteraction_RequestCredential(t *testing.T) { }) t.Run("Fail to log fetch OpenID config metrics event", func(t *testing.T) { issuerServerHandler := &mockIssuerServerHandler{ - t: t, + t: t, + issuerMetadata: "{}", } server := httptest.NewServer(issuerServerHandler) @@ -995,7 +1046,7 @@ func TestIssuerInitiatedInteraction_RequestCredential(t *testing.T) { } config := getTestClientConfig(t) - config.MetricsLogger = &failingMetricsLogger{attemptFailNumber: 1} + config.MetricsLogger = &failingMetricsLogger{attemptFailNumber: 2} interaction, err := openid4ci.NewIssuerInitiatedInteraction( createCredentialOfferIssuanceURI(t, server.URL, false, true), config) @@ -1011,7 +1062,8 @@ func TestIssuerInitiatedInteraction_RequestCredential(t *testing.T) { }) t.Run("Fail to log fetch token via HTTP POST metrics event", func(t *testing.T) { issuerServerHandler := &mockIssuerServerHandler{ - t: t, + t: t, + issuerMetadata: "{}", } server := httptest.NewServer(issuerServerHandler) @@ -1022,7 +1074,7 @@ func TestIssuerInitiatedInteraction_RequestCredential(t *testing.T) { } config := getTestClientConfig(t) - config.MetricsLogger = &failingMetricsLogger{attemptFailNumber: 2} + config.MetricsLogger = &failingMetricsLogger{attemptFailNumber: 3} interaction, err := openid4ci.NewIssuerInitiatedInteraction( createCredentialOfferIssuanceURI(t, server.URL, false, true), config) @@ -1037,7 +1089,8 @@ func TestIssuerInitiatedInteraction_RequestCredential(t *testing.T) { }) t.Run("Fail to log fetch metadata via HTTP GET metrics event", func(t *testing.T) { issuerServerHandler := &mockIssuerServerHandler{ - t: t, + t: t, + issuerMetadata: "{}", } server := httptest.NewServer(issuerServerHandler) @@ -1048,7 +1101,7 @@ func TestIssuerInitiatedInteraction_RequestCredential(t *testing.T) { } config := getTestClientConfig(t) - config.MetricsLogger = &failingMetricsLogger{attemptFailNumber: 3} + config.MetricsLogger = &failingMetricsLogger{attemptFailNumber: 1} interaction, err := openid4ci.NewIssuerInitiatedInteraction( createCredentialOfferIssuanceURI(t, server.URL, false, true), config) @@ -1096,54 +1149,117 @@ func TestIssuerInitiatedInteraction_RequestCredential(t *testing.T) { }) t.Run("Auth flow", func(t *testing.T) { t.Run("Success", func(t *testing.T) { - t.Run("Issuer state specified in credential offer", func(t *testing.T) { - issuerServerHandler := &mockIssuerServerHandler{ - t: t, - credentialResponse: sampleCredentialResponse, - } - - server := httptest.NewServer(issuerServerHandler) - defer server.Close() - - issuerServerHandler.openIDConfig = &openid4ci.OpenIDConfig{ - TokenEndpoint: fmt.Sprintf("%s/oidc/token", server.URL), - } - - issuerServerHandler.issuerMetadata = fmt.Sprintf(`{"credential_endpoint":"%s/credential"}`, - server.URL) - - interaction := newIssuerInitiatedInteraction(t, createCredentialOfferIssuanceURI(t, server.URL, true, true)) - - // Needed to create the OAuth2 config object. - authURL, err := interaction.CreateAuthorizationURL("clientID", "redirectURI") - require.NoError(t, err) - - redirectURIWithParams := "redirectURI?code=1234&state=" + getStateFromAuthURL(t, authURL) - - credentials, err := interaction.RequestCredentialWithAuth(&jwtSignerMock{ - keyID: mockKeyID, - }, redirectURIWithParams) - require.NoError(t, err) - require.Len(t, credentials, 1) - require.NotEmpty(t, credentials[0]) + t.Run("Token endpoint defined in the OpenID configuration", func(t *testing.T) { + t.Run("Issuer state specified in credential offer", func(t *testing.T) { + issuerServerHandler := &mockIssuerServerHandler{ + t: t, + credentialResponse: sampleCredentialResponse, + } + + server := httptest.NewServer(issuerServerHandler) + defer server.Close() + + issuerServerHandler.openIDConfig = &openid4ci.OpenIDConfig{ + TokenEndpoint: fmt.Sprintf("%s/oidc/token", server.URL), + } + + issuerServerHandler.issuerMetadata = fmt.Sprintf(`{"credential_endpoint":"%s/credential"}`, + server.URL) + + interaction := newIssuerInitiatedInteraction(t, createCredentialOfferIssuanceURI(t, server.URL, true, true)) + + // Needed to create the OAuth2 config object. + authURL, err := interaction.CreateAuthorizationURL("clientID", "redirectURI") + require.NoError(t, err) + + redirectURIWithParams := "redirectURI?code=1234&state=" + getStateFromAuthURL(t, authURL) + + credentials, err := interaction.RequestCredentialWithAuth(&jwtSignerMock{ + keyID: mockKeyID, + }, redirectURIWithParams) + require.NoError(t, err) + require.Len(t, credentials, 1) + require.NotEmpty(t, credentials[0]) + }) + t.Run("Issuer state not specified in credential offer", func(t *testing.T) { + issuerServerHandler := &mockIssuerServerHandler{ + t: t, + credentialResponse: sampleCredentialResponse, + } + + server := httptest.NewServer(issuerServerHandler) + defer server.Close() + + issuerServerHandler.openIDConfig = &openid4ci.OpenIDConfig{ + TokenEndpoint: fmt.Sprintf("%s/oidc/token", server.URL), + } + + issuerServerHandler.issuerMetadata = fmt.Sprintf(`{"credential_endpoint":"%s/credential"}`, + server.URL) + + interaction := newIssuerInitiatedInteraction(t, createCredentialOfferIssuanceURI(t, server.URL, true, false)) + + // Needed to create the OAuth2 config object. + authURL, err := interaction.CreateAuthorizationURL("clientID", "redirectURI") + require.NoError(t, err) + + redirectURIWithParams := "redirectURI?code=1234&state=" + getStateFromAuthURL(t, authURL) + + credentials, err := interaction.RequestCredentialWithAuth(&jwtSignerMock{ + keyID: mockKeyID, + }, redirectURIWithParams) + require.NoError(t, err) + require.Len(t, credentials, 1) + require.NotEmpty(t, credentials[0]) + }) + t.Run("Issuer state not specified in credential offer, but is specified by caller in "+ + "CreateAuthorizationURL call", func(t *testing.T) { + issuerServerHandler := &mockIssuerServerHandler{ + t: t, + credentialResponse: sampleCredentialResponse, + } + + server := httptest.NewServer(issuerServerHandler) + defer server.Close() + + issuerServerHandler.openIDConfig = &openid4ci.OpenIDConfig{ + TokenEndpoint: fmt.Sprintf("%s/oidc/token", server.URL), + } + + issuerServerHandler.issuerMetadata = fmt.Sprintf(`{"credential_endpoint":"%s/credential"}`, + server.URL) + + interaction := newIssuerInitiatedInteraction(t, createCredentialOfferIssuanceURI(t, server.URL, true, false)) + + // Needed to create the OAuth2 config object. + authURL, err := interaction.CreateAuthorizationURL("clientID", "redirectURI", + openid4ci.WithIssuerState("IssuerState")) + require.NoError(t, err) + + redirectURIWithParams := "redirectURI?code=1234&state=" + getStateFromAuthURL(t, authURL) + + credentials, err := interaction.RequestCredentialWithAuth(&jwtSignerMock{ + keyID: mockKeyID, + }, redirectURIWithParams) + require.NoError(t, err) + require.Len(t, credentials, 1) + require.NotEmpty(t, credentials[0]) + }) }) - t.Run("Issuer state not specified in credential offer", func(t *testing.T) { + t.Run("Token endpoint defined in the OpenID configuration", func(t *testing.T) { issuerServerHandler := &mockIssuerServerHandler{ t: t, credentialResponse: sampleCredentialResponse, + openIDConfig: &openid4ci.OpenIDConfig{}, } server := httptest.NewServer(issuerServerHandler) defer server.Close() - issuerServerHandler.openIDConfig = &openid4ci.OpenIDConfig{ - TokenEndpoint: fmt.Sprintf("%s/oidc/token", server.URL), - } + issuerServerHandler.issuerMetadata = fmt.Sprintf(`{"credential_endpoint":"%s/credential",`+ + `"token_endpoint":"%s/oidc/token"}`, server.URL, server.URL) - issuerServerHandler.issuerMetadata = fmt.Sprintf(`{"credential_endpoint":"%s/credential"}`, - server.URL) - - interaction := newIssuerInitiatedInteraction(t, createCredentialOfferIssuanceURI(t, server.URL, true, false)) + interaction := newIssuerInitiatedInteraction(t, createCredentialOfferIssuanceURI(t, server.URL, true, true)) // Needed to create the OAuth2 config object. authURL, err := interaction.CreateAuthorizationURL("clientID", "redirectURI") @@ -1151,39 +1267,6 @@ func TestIssuerInitiatedInteraction_RequestCredential(t *testing.T) { redirectURIWithParams := "redirectURI?code=1234&state=" + getStateFromAuthURL(t, authURL) - credentials, err := interaction.RequestCredentialWithAuth(&jwtSignerMock{ - keyID: mockKeyID, - }, redirectURIWithParams) - require.NoError(t, err) - require.Len(t, credentials, 1) - require.NotEmpty(t, credentials[0]) - }) - t.Run("Issuer state not specified in credential offer, but is specified by caller in "+ - "CreateAuthorizationURL call", func(t *testing.T) { - issuerServerHandler := &mockIssuerServerHandler{ - t: t, - credentialResponse: sampleCredentialResponse, - } - - server := httptest.NewServer(issuerServerHandler) - defer server.Close() - - issuerServerHandler.openIDConfig = &openid4ci.OpenIDConfig{ - TokenEndpoint: fmt.Sprintf("%s/oidc/token", server.URL), - } - - issuerServerHandler.issuerMetadata = fmt.Sprintf(`{"credential_endpoint":"%s/credential"}`, - server.URL) - - interaction := newIssuerInitiatedInteraction(t, createCredentialOfferIssuanceURI(t, server.URL, true, false)) - - // Needed to create the OAuth2 config object. - authURL, err := interaction.CreateAuthorizationURL("clientID", "redirectURI", - openid4ci.WithIssuerState("IssuerState")) - require.NoError(t, err) - - redirectURIWithParams := "redirectURI?code=1234&state=" + getStateFromAuthURL(t, authURL) - credentials, err := interaction.RequestCredentialWithAuth(&jwtSignerMock{ keyID: mockKeyID, }, redirectURIWithParams) @@ -1291,7 +1374,8 @@ func TestIssuerInitiatedInteraction_RequestCredential(t *testing.T) { require.EqualError(t, err, `parse "%": invalid URL escape "%"`) require.Nil(t, credentials) }) - t.Run("Fail to fetch OpenID config", func(t *testing.T) { + t.Run("No token endpoint available - couldn't fetch OpenID configuration and the "+ + "issuer's metadata (the fallback) doesn't specify one", func(t *testing.T) { issuerServerHandler := &mockIssuerServerHandler{ t: t, credentialResponse: sampleCredentialResponse, @@ -1319,9 +1403,10 @@ func TestIssuerInitiatedInteraction_RequestCredential(t *testing.T) { credentials, err := interaction.RequestCredentialWithAuth(&jwtSignerMock{ keyID: mockKeyID, }, redirectURIWithParams) - require.EqualError(t, err, "ISSUER_OPENID_CONFIG_FETCH_FAILED(OCI1-0003):failed to fetch issuer's "+ - "OpenID configuration: openid configuration endpoint: expected status code 200 but got status "+ - "code 500 with response body test failure instead") + require.EqualError(t, err, "NO_TOKEN_ENDPOINT_AVAILABLE(OCI1-0019):no token endpoint available. "+ + "An OpenID configuration couldn't be fetched, and the issuer's metadata doesn't specify a token "+ + "endpoint. OpenID configuration fetch error: openid configuration endpoint: expected status code 200 "+ + "but got status code 500 with response body test failure instead") require.Nil(t, credentials) }) t.Run("Fail to create claims proof", func(t *testing.T) { diff --git a/pkg/openid4ci/walletinitiatedinteraction.go b/pkg/openid4ci/walletinitiatedinteraction.go index ee979ad2..747b35fd 100644 --- a/pkg/openid4ci/walletinitiatedinteraction.go +++ b/pkg/openid4ci/walletinitiatedinteraction.go @@ -67,12 +67,12 @@ type SupportedCredential struct { // SupportedCredentials returns the credential types and formats that an issuer can issue. func (i *WalletInitiatedInteraction) SupportedCredentials() ([]SupportedCredential, error) { - issuerMetadata, err := i.interaction.getIssuerMetadata() + err := i.interaction.populateIssuerMetadata("Get supported credentials") if err != nil { return nil, err } - supportedCredentials := make([]SupportedCredential, len(issuerMetadata.CredentialsSupported)) + supportedCredentials := make([]SupportedCredential, len(i.interaction.issuerMetadata.CredentialsSupported)) for j := 0; j < len(i.interaction.issuerMetadata.CredentialsSupported); j++ { supportedCredentials[j] = SupportedCredential{ @@ -137,5 +137,10 @@ func (i *WalletInitiatedInteraction) DynamicClientRegistrationEndpoint() (string // IssuerMetadata returns the issuer's metadata. func (i *WalletInitiatedInteraction) IssuerMetadata() (*issuer.Metadata, error) { - return i.interaction.getIssuerMetadata() + err := i.interaction.populateIssuerMetadata(getIssuerMetadataEventText) + if err != nil { + return nil, err + } + + return i.interaction.issuerMetadata, nil } diff --git a/pkg/openid4ci/walletinitiatedinteraction_test.go b/pkg/openid4ci/walletinitiatedinteraction_test.go index e6d19b52..8edcf749 100644 --- a/pkg/openid4ci/walletinitiatedinteraction_test.go +++ b/pkg/openid4ci/walletinitiatedinteraction_test.go @@ -11,58 +11,140 @@ import ( "net/http/httptest" "testing" - "github.com/trustbloc/wallet-sdk/pkg/openid4ci" - "github.com/stretchr/testify/require" + "github.com/trustbloc/wallet-sdk/pkg/openid4ci" ) -func TestWalletInitiatedInteraction(t *testing.T) { - issuerServerHandler := &mockIssuerServerHandler{ - t: t, - credentialResponse: sampleCredentialResponse, - } +func TestWalletInitiatedInteractionFlow(t *testing.T) { + t.Run("Token endpoint defined in the OpenID configuration", func(t *testing.T) { + issuerServerHandler := &mockIssuerServerHandler{ + t: t, + credentialResponse: sampleCredentialResponse, + } + + server := httptest.NewServer(issuerServerHandler) + defer server.Close() + + issuerServerHandler.openIDConfig = &openid4ci.OpenIDConfig{ + TokenEndpoint: fmt.Sprintf("%s/oidc/token", server.URL), + } + + issuerServerHandler.issuerMetadata = fmt.Sprintf(`{"credential_endpoint":"%s/credential"}`, + server.URL) + + config := getTestClientConfig(t) + + interaction, err := openid4ci.NewWalletInitiatedInteraction(server.URL, config) + require.NoError(t, err) + + supportedCredentials, err := interaction.SupportedCredentials() + require.NoError(t, err) + require.NotNil(t, supportedCredentials) + + dynamicClientRegistrationSupported, err := interaction.DynamicClientRegistrationSupported() + require.NoError(t, err) + require.False(t, dynamicClientRegistrationSupported) + + dynamicClientRegistrationEndpoint, err := interaction.DynamicClientRegistrationEndpoint() + require.EqualError(t, err, + "INVALID_SDK_USAGE(OCI3-0000):issuer does not support dynamic client registration") + require.Empty(t, dynamicClientRegistrationEndpoint) + + types := []string{"VerifiableCredential", "VerifiedEmployee"} - server := httptest.NewServer(issuerServerHandler) - defer server.Close() + // Needed to create the OAuth2 config object. + authURL, err := interaction.CreateAuthorizationURL("clientID", "redirectURI", + "jwt_vc_json", types, openid4ci.WithIssuerState("issuerState")) + require.NoError(t, err) - issuerServerHandler.openIDConfig = &openid4ci.OpenIDConfig{ - TokenEndpoint: fmt.Sprintf("%s/oidc/token", server.URL), - } + redirectURIWithParams := "redirectURI?code=1234&state=" + getStateFromAuthURL(t, authURL) - issuerServerHandler.issuerMetadata = fmt.Sprintf(`{"credential_endpoint":"%s/credential"}`, - server.URL) + credentials, err := interaction.RequestCredential(&jwtSignerMock{ + keyID: mockKeyID, + }, redirectURIWithParams) + require.NoError(t, err) + require.Len(t, credentials, 1) + require.NotEmpty(t, credentials[0]) + }) + t.Run("Token endpoint defined in the issuer's metadata", func(t *testing.T) { + issuerServerHandler := &mockIssuerServerHandler{ + t: t, + credentialResponse: sampleCredentialResponse, + openIDConfig: &openid4ci.OpenIDConfig{}, + } - config := getTestClientConfig(t) + server := httptest.NewServer(issuerServerHandler) + defer server.Close() + + issuerServerHandler.issuerMetadata = fmt.Sprintf(`{"credential_endpoint":"%s/credential",`+ + `"token_endpoint":"%s/oidc/token"}`, server.URL, server.URL) + + config := getTestClientConfig(t) + + interaction, err := openid4ci.NewWalletInitiatedInteraction(server.URL, config) + require.NoError(t, err) + + supportedCredentials, err := interaction.SupportedCredentials() + require.NoError(t, err) + require.NotNil(t, supportedCredentials) + + dynamicClientRegistrationSupported, err := interaction.DynamicClientRegistrationSupported() + require.NoError(t, err) + require.False(t, dynamicClientRegistrationSupported) + + dynamicClientRegistrationEndpoint, err := interaction.DynamicClientRegistrationEndpoint() + require.EqualError(t, err, + "INVALID_SDK_USAGE(OCI3-0000):issuer does not support dynamic client registration") + require.Empty(t, dynamicClientRegistrationEndpoint) + + types := []string{"VerifiableCredential", "VerifiedEmployee"} + + // Needed to create the OAuth2 config object. + authURL, err := interaction.CreateAuthorizationURL("clientID", "redirectURI", + "jwt_vc_json", types, openid4ci.WithIssuerState("issuerState")) + require.NoError(t, err) + + redirectURIWithParams := "redirectURI?code=1234&state=" + getStateFromAuthURL(t, authURL) + + credentials, err := interaction.RequestCredential(&jwtSignerMock{ + keyID: mockKeyID, + }, redirectURIWithParams) + require.NoError(t, err) + require.Len(t, credentials, 1) + require.NotEmpty(t, credentials[0]) + }) +} - interaction, err := openid4ci.NewWalletInitiatedInteraction(server.URL, config) - require.NoError(t, err) +func TestWalletInitiatedInteraction_IssuerMetadata(t *testing.T) { + t.Run("Success", func(t *testing.T) { + issuerServerHandler := &mockIssuerServerHandler{ + t: t, + } - supportedCredentials, err := interaction.SupportedCredentials() - require.NoError(t, err) - require.NotNil(t, supportedCredentials) + server := httptest.NewServer(issuerServerHandler) + defer server.Close() - dynamicClientRegistrationSupported, err := interaction.DynamicClientRegistrationSupported() - require.NoError(t, err) - require.False(t, dynamicClientRegistrationSupported) + issuerServerHandler.issuerMetadata = fmt.Sprintf(`{"credential_endpoint":"%s/credential",`+ + `"token_endpoint":"%s/oidc/token"}`, server.URL, server.URL) - dynamicClientRegistrationEndpoint, err := interaction.DynamicClientRegistrationEndpoint() - require.EqualError(t, err, - "INVALID_SDK_USAGE(OCI3-0000):issuer does not support dynamic client registration") - require.Empty(t, dynamicClientRegistrationEndpoint) + config := getTestClientConfig(t) - types := []string{"VerifiableCredential", "VerifiedEmployee"} + interaction, err := openid4ci.NewWalletInitiatedInteraction(server.URL, config) + require.NoError(t, err) - // Needed to create the OAuth2 config object. - authURL, err := interaction.CreateAuthorizationURL("clientID", "redirectURI", - "jwt_vc_json", types, openid4ci.WithIssuerState("issuerState")) - require.NoError(t, err) + issuerMetadata, err := interaction.IssuerMetadata() + require.NoError(t, err) + require.NotNil(t, issuerMetadata) + }) + t.Run("Fail to fetch issuer metadata", func(t *testing.T) { + config := getTestClientConfig(t) - redirectURIWithParams := "redirectURI?code=1234&state=" + getStateFromAuthURL(t, authURL) + interaction, err := openid4ci.NewWalletInitiatedInteraction("", config) + require.NoError(t, err) - credentials, err := interaction.RequestCredential(&jwtSignerMock{ - keyID: mockKeyID, - }, redirectURIWithParams) - require.NoError(t, err) - require.Len(t, credentials, 1) - require.NotEmpty(t, credentials[0]) + issuerMetadata, err := interaction.IssuerMetadata() + require.EqualError(t, err, "METADATA_FETCH_FAILED(OCI1-0004):failed to get issuer metadata: "+ + `openid configuration endpoint: Get "/.well-known/openid-credential-issuer": unsupported protocol scheme ""`) + require.Nil(t, issuerMetadata) + }) } diff --git a/test/integration/pkg/helpers/openid4ci.go b/test/integration/pkg/helpers/openid4ci.go index ad08b1a9..ec67a911 100644 --- a/test/integration/pkg/helpers/openid4ci.go +++ b/test/integration/pkg/helpers/openid4ci.go @@ -103,12 +103,12 @@ func (h *CITestHelper) CheckMetricsLoggerAfterOpenID4CIFlow(t *testing.T, issuer checkInteractionInstantiationMetricsEvent(t, h.MetricsLogger.Events[0]) - checkFetchOpenIDConfigMetricsEvent(t, h.MetricsLogger.Events[1], issuerProfileID) + checkFetchIssuerMetadataMetricsEvent(t, h.MetricsLogger.Events[1], + "Request credential(s) from issuer", issuerProfileID) - checkFetchTokenMetricsEvent(t, h.MetricsLogger.Events[2]) + checkFetchOpenIDConfigMetricsEvent(t, h.MetricsLogger.Events[2], issuerProfileID) - checkFetchIssuerMetadataMetricsEvent(t, h.MetricsLogger.Events[3], - "Request credential(s) from issuer", issuerProfileID) + checkFetchTokenMetricsEvent(t, h.MetricsLogger.Events[3]) checkFetchCredentialHTTPRequestMetricsEvent(t, h.MetricsLogger.Events[4])