diff --git a/api/spec/openapi.gen.go b/api/spec/openapi.gen.go index 8093059c2..834ea9c36 100644 --- a/api/spec/openapi.gen.go +++ b/api/spec/openapi.gen.go @@ -19,153 +19,157 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "1gAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/cmd/vc-rest/startcmd/start.go b/cmd/vc-rest/startcmd/start.go index 6500622a5..7224a3464 100644 --- a/cmd/vc-rest/startcmd/start.go +++ b/cmd/vc-rest/startcmd/start.go @@ -83,6 +83,7 @@ import ( oidc4vpv1 "github.com/trustbloc/vcs/pkg/restapi/v1/oidc4vp" verifierv1 "github.com/trustbloc/vcs/pkg/restapi/v1/verifier" "github.com/trustbloc/vcs/pkg/restapi/v1/version" + "github.com/trustbloc/vcs/pkg/service/attestation" "github.com/trustbloc/vcs/pkg/service/clientidscheme" clientmanagersvc "github.com/trustbloc/vcs/pkg/service/clientmanager" credentialstatustypes "github.com/trustbloc/vcs/pkg/service/credentialstatus" @@ -667,6 +668,12 @@ func buildEchoHandler( jsonSchemaValidator := jsonschema.NewCachingValidator() + attestationService := attestation.NewService( + &attestation.Config{ + HTTPClient: getHTTPClient(metricsProvider.ClientAttestationService), + }, + ) + oidc4ciService, err = oidc4ci.NewService(&oidc4ci.Config{ TransactionStore: oidc4ciTransactionStore, ClaimDataStore: oidc4ciClaimDataStore, @@ -683,6 +690,7 @@ func buildEchoHandler( KMSRegistry: kmsRegistry, CryptoJWTSigner: vcCrypto, JSONSchemaValidator: jsonSchemaValidator, + AttestationService: attestationService, }) if err != nil { return nil, fmt.Errorf("failed to instantiate new oidc4ci service: %w", err) diff --git a/docs/v1/openapi.yaml b/docs/v1/openapi.yaml index 375e92eca..ae60f748c 100644 --- a/docs/v1/openapi.yaml +++ b/docs/v1/openapi.yaml @@ -906,16 +906,16 @@ components: description: The Credential Issuer's identifier. credential_endpoint: type: string - description: URL of the Credential Issuer's Credential Endpoint. This URL MUST use the https scheme and MAY contain port, path and query parameter components. + description: 'URL of the Credential Issuer''s Credential Endpoint. This URL MUST use the https scheme and MAY contain port, path and query parameter components.' batch_credential_endpoint: type: string - description: URL of the Credential Issuer's Batch Credential Endpoint. This URL MUST use the https scheme and MAY contain port, path and query parameter components. If omitted, the Credential Issuer does not support the Batch Credential Endpoint. + description: 'URL of the Credential Issuer''s Batch Credential Endpoint. This URL MUST use the https scheme and MAY contain port, path and query parameter components. If omitted, the Credential Issuer does not support the Batch Credential Endpoint.' credentials_supported: type: array - description: A JSON array containing a list of JSON objects, each of them representing metadata about a separate credential type that the Credential Issuer can issue. + description: 'A JSON array containing a list of JSON objects, each of them representing metadata about a separate credential type that the Credential Issuer can issue.' display: type: array - description: An array of objects, where each object contains display properties of a Credential Issuer for a certain language. + description: 'An array of objects, where each object contains display properties of a Credential Issuer for a certain language.' items: $ref: '#/components/schemas/CredentialDisplay' token_endpoint_auth_methods_supported: @@ -1283,6 +1283,15 @@ components: properties: op_state: type: string + client_id: + type: string + description: Client ID for VCS OIDC interaction. + client_assertion_type: + type: string + description: 'Specifies the method used to authenticate the client application to the authorization server (VCS). The only supported value is "urn:ietf:params:oauth:client-assertion-type:jwt-client-attestation". It indicates that the client must authenticate using OAuth 2.0 Attestation-Based Client Authentication method.' + client_assertion: + type: string + description: 'The value MUST contain two JWTs, separated by a "~" character. The first JWT is the client attestation JWT, the second is the client attestation PoP JWT.' required: - op_state ExchangeAuthorizationCodeResponse: @@ -1337,7 +1346,13 @@ components: description: User pin. client_id: type: string - description: Client ID. + description: Client ID for VCS OIDC interaction. + client_assertion_type: + type: string + description: 'Specifies the method used to authenticate the client application to the authorization server (VCS). The only supported value is "urn:ietf:params:oauth:client-assertion-type:jwt-client-attestation". It indicates that the client must authenticate using OAuth 2.0 Attestation-Based Client Authentication method.' + client_assertion: + type: string + description: 'The value MUST contain two JWTs, separated by a "~" character. The first JWT is the client attestation JWT, the second is the client attestation PoP JWT.' required: - pre-authorized_code ValidatePreAuthorizedCodeResponse: diff --git a/pkg/observability/metrics/prometheus/provider.go b/pkg/observability/metrics/prometheus/provider.go index bc26a7ccb..f1f13e772 100644 --- a/pkg/observability/metrics/prometheus/provider.go +++ b/pkg/observability/metrics/prometheus/provider.go @@ -120,6 +120,7 @@ func NewMetrics( metrics.ClientOIDC4CI, metrics.ClientOIDC4CIV1, metrics.ClientWellKnown, metrics.ClientCredentialVerifier, metrics.ClientDiscoverableClientIDScheme, + metrics.ClientAttestationService, } pm := &PromMetrics{ diff --git a/pkg/observability/metrics/provider.go b/pkg/observability/metrics/provider.go index d1c9cb8d4..63bab0974 100644 --- a/pkg/observability/metrics/provider.go +++ b/pkg/observability/metrics/provider.go @@ -58,6 +58,7 @@ const ( ClientIssuerInteraction ClientID = "issuer-interaction" ClientCredentialVerifier ClientID = "credential-verifier" //nolint:gosec ClientDiscoverableClientIDScheme ClientID = "discoverable-client-id-scheme" + ClientAttestationService ClientID = "attestation-service" ) // Provider is an interface for metrics provider. diff --git a/pkg/observability/tracing/wrappers/oidc4ci/oidc4ci_wrapper.go b/pkg/observability/tracing/wrappers/oidc4ci/oidc4ci_wrapper.go index 281f46a5f..7628955d1 100644 --- a/pkg/observability/tracing/wrappers/oidc4ci/oidc4ci_wrapper.go +++ b/pkg/observability/tracing/wrappers/oidc4ci/oidc4ci_wrapper.go @@ -70,11 +70,11 @@ func (w *Wrapper) StoreAuthorizationCode(ctx context.Context, opState string, co return w.svc.StoreAuthorizationCode(ctx, opState, code, flowData) } -func (w *Wrapper) ExchangeAuthorizationCode(ctx context.Context, opState string) (oidc4ci.TxID, error) { - return w.svc.ExchangeAuthorizationCode(ctx, opState) +func (w *Wrapper) ExchangeAuthorizationCode(ctx context.Context, opState, clientID, clientAttestationType, clientAttestation string) (oidc4ci.TxID, error) { + return w.svc.ExchangeAuthorizationCode(ctx, opState, clientID, clientAttestationType, clientAttestation) } -func (w *Wrapper) ValidatePreAuthorizedCodeRequest(ctx context.Context, preAuthorizedCode, pin, clientID string) (*oidc4ci.Transaction, error) { +func (w *Wrapper) ValidatePreAuthorizedCodeRequest(ctx context.Context, preAuthorizedCode, pin, clientID, clientAttestationType, clientAttestation string) (*oidc4ci.Transaction, error) { ctx, span := w.tracer.Start(ctx, "oidc4ci.ValidatePreAuthorizedCodeRequest") defer span.End() @@ -82,7 +82,7 @@ func (w *Wrapper) ValidatePreAuthorizedCodeRequest(ctx context.Context, preAutho span.SetAttributes(attribute.String("pin", pin)) span.SetAttributes(attribute.String("client_id", clientID)) - tx, err := w.svc.ValidatePreAuthorizedCodeRequest(ctx, preAuthorizedCode, pin, clientID) + tx, err := w.svc.ValidatePreAuthorizedCodeRequest(ctx, preAuthorizedCode, pin, clientID, clientAttestationType, clientAttestation) if err != nil { return nil, err } diff --git a/pkg/observability/tracing/wrappers/oidc4ci/oidc4ci_wrapper_test.go b/pkg/observability/tracing/wrappers/oidc4ci/oidc4ci_wrapper_test.go index d2a89588e..ddb42a878 100644 --- a/pkg/observability/tracing/wrappers/oidc4ci/oidc4ci_wrapper_test.go +++ b/pkg/observability/tracing/wrappers/oidc4ci/oidc4ci_wrapper_test.go @@ -77,11 +77,11 @@ func TestWrapper_ExchangeAuthorizationCode(t *testing.T) { ctrl := gomock.NewController(t) svc := NewMockService(ctrl) - svc.EXPECT().ExchangeAuthorizationCode(gomock.Any(), "opState").Times(1) + svc.EXPECT().ExchangeAuthorizationCode(gomock.Any(), "opState", "", "", "").Times(1) w := Wrap(svc, trace.NewNoopTracerProvider().Tracer("")) - _, err := w.ExchangeAuthorizationCode(context.Background(), "opState") + _, err := w.ExchangeAuthorizationCode(context.Background(), "opState", "", "", "") require.NoError(t, err) } @@ -89,11 +89,11 @@ func TestWrapper_ValidatePreAuthorizedCodeRequest(t *testing.T) { ctrl := gomock.NewController(t) svc := NewMockService(ctrl) - svc.EXPECT().ValidatePreAuthorizedCodeRequest(gomock.Any(), "code", "pin", "clientID").Return(&oidc4ci.Transaction{ID: "id"}, nil) + svc.EXPECT().ValidatePreAuthorizedCodeRequest(gomock.Any(), "code", "pin", "clientID", "", "").Return(&oidc4ci.Transaction{ID: "id"}, nil) w := Wrap(svc, trace.NewNoopTracerProvider().Tracer("")) - _, err := w.ValidatePreAuthorizedCodeRequest(context.Background(), "code", "pin", "clientID") + _, err := w.ValidatePreAuthorizedCodeRequest(context.Background(), "code", "pin", "clientID", "", "") require.NoError(t, err) } diff --git a/pkg/restapi/resterr/error.go b/pkg/restapi/resterr/error.go index ab343845e..24e4a5e69 100644 --- a/pkg/restapi/resterr/error.go +++ b/pkg/restapi/resterr/error.go @@ -30,6 +30,7 @@ const ( OIDCPreAuthorizeInvalidClientID ErrorCode = "oidc-pre-authorize-invalid-client-id" OIDCCredentialFormatNotSupported ErrorCode = "oidc-credential-format-not-supported" OIDCCredentialTypeNotSupported ErrorCode = "oidc-credential-type-not-supported" + OIDCClientAuthenticationFailed ErrorCode = "oidc-client-authentication-failed" InvalidOrMissingProofOIDCErr ErrorCode = "invalid_or_missing_proof" ProfileNotFound ErrorCode = "profile-not-found" diff --git a/pkg/restapi/v1/issuer/controller.go b/pkg/restapi/v1/issuer/controller.go index 75b0f60d0..bf1ad8853 100644 --- a/pkg/restapi/v1/issuer/controller.go +++ b/pkg/restapi/v1/issuer/controller.go @@ -665,7 +665,12 @@ func (c *Controller) ExchangeAuthorizationCodeRequest(ctx echo.Context) error { return err } - txID, err := c.oidc4ciService.ExchangeAuthorizationCode(ctx.Request().Context(), body.OpState) + txID, err := c.oidc4ciService.ExchangeAuthorizationCode(ctx.Request().Context(), + body.OpState, + lo.FromPtr(body.ClientId), + lo.FromPtr(body.ClientAssertionType), + lo.FromPtr(body.ClientAssertion), + ) if err != nil { return util.WriteOutput(ctx)(nil, err) } @@ -683,8 +688,12 @@ func (c *Controller) ValidatePreAuthorizedCodeRequest(ctx echo.Context) error { } result, err := c.oidc4ciService.ValidatePreAuthorizedCodeRequest(ctx.Request().Context(), - body.PreAuthorizedCode, lo.FromPtr(body.UserPin), lo.FromPtr(body.ClientId)) - + body.PreAuthorizedCode, + lo.FromPtr(body.UserPin), + lo.FromPtr(body.ClientId), + lo.FromPtr(body.ClientAssertionType), + lo.FromPtr(body.ClientAssertion), + ) if err != nil { return err } diff --git a/pkg/restapi/v1/issuer/controller_test.go b/pkg/restapi/v1/issuer/controller_test.go index d56c41d48..e0df37b7a 100644 --- a/pkg/restapi/v1/issuer/controller_test.go +++ b/pkg/restapi/v1/issuer/controller_test.go @@ -1206,7 +1206,8 @@ func TestController_ExchangeAuthorizationCode(t *testing.T) { t.Run("success", func(t *testing.T) { opState := uuid.NewString() mockOIDC4CIService := NewMockOIDC4CIService(gomock.NewController(t)) - mockOIDC4CIService.EXPECT().ExchangeAuthorizationCode(gomock.Any(), opState).Return(oidc4ci.TxID("1234"), nil) + mockOIDC4CIService.EXPECT().ExchangeAuthorizationCode(gomock.Any(), opState, "", "", ""). + Return(oidc4ci.TxID("1234"), nil) c := &Controller{ oidc4ciService: mockOIDC4CIService, @@ -1220,7 +1221,7 @@ func TestController_ExchangeAuthorizationCode(t *testing.T) { t.Run("error from service", func(t *testing.T) { opState := uuid.NewString() mockOIDC4CIService := NewMockOIDC4CIService(gomock.NewController(t)) - mockOIDC4CIService.EXPECT().ExchangeAuthorizationCode(gomock.Any(), opState). + mockOIDC4CIService.EXPECT().ExchangeAuthorizationCode(gomock.Any(), opState, "", "", ""). Return(oidc4ci.TxID(""), errors.New("unexpected error")) c := &Controller{ @@ -1244,7 +1245,7 @@ func TestController_ExchangeAuthorizationCode(t *testing.T) { func TestController_ValidatePreAuthorizedCodeRequest(t *testing.T) { t.Run("success with pin", func(t *testing.T) { mockOIDC4CIService := NewMockOIDC4CIService(gomock.NewController(t)) - mockOIDC4CIService.EXPECT().ValidatePreAuthorizedCodeRequest(gomock.Any(), "1234", "5432", "123"). + mockOIDC4CIService.EXPECT().ValidatePreAuthorizedCodeRequest(gomock.Any(), "1234", "5432", "123", "", ""). Return(&oidc4ci.Transaction{ TransactionData: oidc4ci.TransactionData{ OpState: "random_op_state", @@ -1263,7 +1264,7 @@ func TestController_ValidatePreAuthorizedCodeRequest(t *testing.T) { t.Run("success without pin", func(t *testing.T) { mockOIDC4CIService := NewMockOIDC4CIService(gomock.NewController(t)) - mockOIDC4CIService.EXPECT().ValidatePreAuthorizedCodeRequest(gomock.Any(), "1234", "", "123"). + mockOIDC4CIService.EXPECT().ValidatePreAuthorizedCodeRequest(gomock.Any(), "1234", "", "123", "", ""). Return(&oidc4ci.Transaction{ TransactionData: oidc4ci.TransactionData{ OpState: "random_op_state", @@ -1282,7 +1283,7 @@ func TestController_ValidatePreAuthorizedCodeRequest(t *testing.T) { t.Run("fail with pin", func(t *testing.T) { mockOIDC4CIService := NewMockOIDC4CIService(gomock.NewController(t)) - mockOIDC4CIService.EXPECT().ValidatePreAuthorizedCodeRequest(gomock.Any(), "1234", "5432", "123"). + mockOIDC4CIService.EXPECT().ValidatePreAuthorizedCodeRequest(gomock.Any(), "1234", "5432", "123", "", ""). Return(nil, errors.New("unexpected error")) c := &Controller{ diff --git a/pkg/restapi/v1/issuer/openapi.gen.go b/pkg/restapi/v1/issuer/openapi.gen.go index d3bbc1841..8531190e5 100644 --- a/pkg/restapi/v1/issuer/openapi.gen.go +++ b/pkg/restapi/v1/issuer/openapi.gen.go @@ -64,7 +64,15 @@ type CredentialStatusOpt struct { // Model for exchanging auth code from issuer oauth type ExchangeAuthorizationCodeRequest struct { - OpState string `json:"op_state"` + // The value MUST contain two JWTs, separated by a "~" character. The first JWT is the client attestation JWT, the second is the client attestation PoP JWT. + ClientAssertion *string `json:"client_assertion,omitempty"` + + // Specifies the method used to authenticate the client application to the authorization server (VCS). The only supported value is "urn:ietf:params:oauth:client-assertion-type:jwt-client-attestation". It indicates that the client must authenticate using OAuth 2.0 Attestation-Based Client Authentication method. + ClientAssertionType *string `json:"client_assertion_type,omitempty"` + + // Client ID for VCS OIDC interaction. + ClientId *string `json:"client_id,omitempty"` + OpState string `json:"op_state"` } // Response model for exchanging auth code from issuer oauth @@ -275,7 +283,13 @@ type UpdateCredentialStatusRequest struct { // Model for validating pre-authorized code and pin. type ValidatePreAuthorizedCodeRequest struct { - // Client ID. + // The value MUST contain two JWTs, separated by a "~" character. The first JWT is the client attestation JWT, the second is the client attestation PoP JWT. + ClientAssertion *string `json:"client_assertion,omitempty"` + + // Specifies the method used to authenticate the client application to the authorization server (VCS). The only supported value is "urn:ietf:params:oauth:client-assertion-type:jwt-client-attestation". It indicates that the client must authenticate using OAuth 2.0 Attestation-Based Client Authentication method. + ClientAssertionType *string `json:"client_assertion_type,omitempty"` + + // Client ID for VCS OIDC interaction. ClientId *string `json:"client_id,omitempty"` // Pre authorized code. diff --git a/pkg/restapi/v1/oidc4ci/controller.go b/pkg/restapi/v1/oidc4ci/controller.go index 2134b3cb3..8fbe63341 100644 --- a/pkg/restapi/v1/oidc4ci/controller.go +++ b/pkg/restapi/v1/oidc4ci/controller.go @@ -479,6 +479,8 @@ func (c *Controller) OidcToken(e echo.Context) error { e.FormValue("pre-authorized_code"), e.FormValue("user_pin"), e.FormValue("client_id"), + e.FormValue("client_assertion_type"), + e.FormValue("client_assertion"), ) if preAuthorizeErr != nil { @@ -490,7 +492,10 @@ func (c *Controller) OidcToken(e echo.Context) error { exchangeResp, errExchange := c.issuerInteractionClient.ExchangeAuthorizationCodeRequest( ctx, issuer.ExchangeAuthorizationCodeRequestJSONRequestBody{ - OpState: ar.GetSession().(*fosite.DefaultSession).Extra[sessionOpStateKey].(string), + OpState: ar.GetSession().(*fosite.DefaultSession).Extra[sessionOpStateKey].(string), + ClientId: lo.ToPtr(ar.GetClient().GetID()), + ClientAssertionType: lo.ToPtr(e.FormValue("client_assertion_type")), + ClientAssertion: lo.ToPtr(e.FormValue("client_assertion")), }, ) if errExchange != nil { @@ -725,12 +730,16 @@ func (c *Controller) oidcPreAuthorizedCode( preAuthorizedCode string, userPin string, clientID string, + clientAssertionType string, + clientAssertion string, ) (*issuer.ValidatePreAuthorizedCodeResponse, error) { resp, err := c.issuerInteractionClient.ValidatePreAuthorizedCodeRequest(ctx, issuer.ValidatePreAuthorizedCodeRequestJSONRequestBody{ - PreAuthorizedCode: preAuthorizedCode, - UserPin: lo.ToPtr(userPin), - ClientId: lo.ToPtr(clientID), + PreAuthorizedCode: preAuthorizedCode, + UserPin: lo.ToPtr(userPin), + ClientId: lo.ToPtr(clientID), + ClientAssertionType: lo.ToPtr(clientAssertionType), + ClientAssertion: lo.ToPtr(clientAssertion), }) if err != nil { return nil, err diff --git a/pkg/restapi/v1/oidc4ci/controller_e2e_flows_test.go b/pkg/restapi/v1/oidc4ci/controller_e2e_flows_test.go index 2d80d7af8..c44269c2f 100644 --- a/pkg/restapi/v1/oidc4ci/controller_e2e_flows_test.go +++ b/pkg/restapi/v1/oidc4ci/controller_e2e_flows_test.go @@ -250,9 +250,11 @@ func TestPreAuthorizeCodeGrantFlow(t *testing.T) { interaction.EXPECT().ValidatePreAuthorizedCodeRequest(gomock.Any(), issuer.ValidatePreAuthorizedCodeRequestJSONRequestBody{ - ClientId: lo.ToPtr(clientID), - PreAuthorizedCode: code, - UserPin: lo.ToPtr(pin), + PreAuthorizedCode: code, + UserPin: lo.ToPtr(pin), + ClientId: lo.ToPtr(clientID), + ClientAssertionType: lo.ToPtr(""), + ClientAssertion: lo.ToPtr(""), }, ).Return(&http.Response{ StatusCode: http.StatusOK, @@ -324,7 +326,10 @@ func mockIssuerInteractionClient( client.EXPECT().ExchangeAuthorizationCodeRequest( gomock.Any(), issuer.ExchangeAuthorizationCodeRequestJSONRequestBody{ - OpState: opState, + OpState: opState, + ClientId: lo.ToPtr(clientID), + ClientAssertion: lo.ToPtr(""), + ClientAssertionType: lo.ToPtr(""), }, ).Return(&http.Response{ StatusCode: http.StatusOK, diff --git a/pkg/restapi/v1/oidc4ci/controller_test.go b/pkg/restapi/v1/oidc4ci/controller_test.go index f0049b0f9..24d477818 100644 --- a/pkg/restapi/v1/oidc4ci/controller_test.go +++ b/pkg/restapi/v1/oidc4ci/controller_test.go @@ -839,12 +839,18 @@ func TestController_OidcToken(t *testing.T) { "opState": opState, }, }, + Client: &fosite.DefaultClient{ + ID: clientID, + }, }, }, nil) mockInteractionClient.EXPECT().ExchangeAuthorizationCodeRequest(gomock.Any(), issuer.ExchangeAuthorizationCodeRequestJSONRequestBody{ - OpState: opState, + OpState: opState, + ClientId: lo.ToPtr(clientID), + ClientAssertionType: lo.ToPtr(""), + ClientAssertion: lo.ToPtr(""), }). Return( &http.Response{ @@ -883,6 +889,9 @@ func TestController_OidcToken(t *testing.T) { "opState": "1234", }, }, + Client: &fosite.DefaultClient{ + ID: clientID, + }, }, }, nil) @@ -911,6 +920,9 @@ func TestController_OidcToken(t *testing.T) { "opState": "1234", }, }, + Client: &fosite.DefaultClient{ + ID: clientID, + }, }, }, nil) @@ -1895,9 +1907,11 @@ func TestController_OidcPreAuthorize(t *testing.T) { setup: func() { mockInteractionClient.EXPECT().ValidatePreAuthorizedCodeRequest(gomock.Any(), issuer.ValidatePreAuthorizedCodeRequestJSONRequestBody{ - PreAuthorizedCode: "123456", - UserPin: lo.ToPtr("5678"), - ClientId: lo.ToPtr(clientID), + PreAuthorizedCode: "123456", + UserPin: lo.ToPtr("5678"), + ClientId: lo.ToPtr(clientID), + ClientAssertionType: lo.ToPtr(""), + ClientAssertion: lo.ToPtr(""), }).Return(nil, errors.New("invalid pin")) mockOAuthProvider.EXPECT().NewAccessRequest(gomock.Any(), gomock.Any(), gomock.Any()). @@ -1927,9 +1941,11 @@ func TestController_OidcPreAuthorize(t *testing.T) { }, nil) mockInteractionClient.EXPECT().ValidatePreAuthorizedCodeRequest(gomock.Any(), issuer.ValidatePreAuthorizedCodeRequestJSONRequestBody{ - PreAuthorizedCode: "321", - UserPin: lo.ToPtr(""), - ClientId: lo.ToPtr(clientID), + PreAuthorizedCode: "321", + UserPin: lo.ToPtr(""), + ClientId: lo.ToPtr(clientID), + ClientAssertionType: lo.ToPtr(""), + ClientAssertion: lo.ToPtr(""), }).Return(&http.Response{ Body: io.NopCloser(strings.NewReader("{")), StatusCode: http.StatusOK, @@ -1955,9 +1971,11 @@ func TestController_OidcPreAuthorize(t *testing.T) { }, nil) mockInteractionClient.EXPECT().ValidatePreAuthorizedCodeRequest(gomock.Any(), issuer.ValidatePreAuthorizedCodeRequestJSONRequestBody{ - PreAuthorizedCode: "321", - UserPin: lo.ToPtr(""), - ClientId: lo.ToPtr(clientID), + PreAuthorizedCode: "321", + UserPin: lo.ToPtr(""), + ClientId: lo.ToPtr(clientID), + ClientAssertionType: lo.ToPtr(""), + ClientAssertion: lo.ToPtr(""), }).Return(&http.Response{ Body: io.NopCloser(strings.NewReader("{}")), StatusCode: http.StatusBadRequest, @@ -1983,9 +2001,11 @@ func TestController_OidcPreAuthorize(t *testing.T) { }, nil) mockInteractionClient.EXPECT().ValidatePreAuthorizedCodeRequest(gomock.Any(), issuer.ValidatePreAuthorizedCodeRequestJSONRequestBody{ - PreAuthorizedCode: "321", - UserPin: lo.ToPtr(""), - ClientId: lo.ToPtr(clientID), + PreAuthorizedCode: "321", + UserPin: lo.ToPtr(""), + ClientId: lo.ToPtr(clientID), + ClientAssertionType: lo.ToPtr(""), + ClientAssertion: lo.ToPtr(""), }).Return(&http.Response{ Body: io.NopCloser(strings.NewReader(`{"code": "oidc-tx-not-found"}`)), StatusCode: http.StatusBadRequest, @@ -2013,9 +2033,11 @@ func TestController_OidcPreAuthorize(t *testing.T) { }, nil) mockInteractionClient.EXPECT().ValidatePreAuthorizedCodeRequest(gomock.Any(), issuer.ValidatePreAuthorizedCodeRequestJSONRequestBody{ - PreAuthorizedCode: "321", - UserPin: lo.ToPtr(""), - ClientId: lo.ToPtr(clientID), + PreAuthorizedCode: "321", + UserPin: lo.ToPtr(""), + ClientId: lo.ToPtr(clientID), + ClientAssertionType: lo.ToPtr(""), + ClientAssertion: lo.ToPtr(""), }).Return(&http.Response{ Body: io.NopCloser(strings.NewReader(`{"code": "oidc-pre-authorize-expect-pin"}`)), StatusCode: http.StatusBadRequest, @@ -2042,9 +2064,11 @@ func TestController_OidcPreAuthorize(t *testing.T) { }, nil) mockInteractionClient.EXPECT().ValidatePreAuthorizedCodeRequest(gomock.Any(), issuer.ValidatePreAuthorizedCodeRequestJSONRequestBody{ - PreAuthorizedCode: "321", - UserPin: lo.ToPtr(""), - ClientId: lo.ToPtr(""), + PreAuthorizedCode: "321", + UserPin: lo.ToPtr(""), + ClientId: lo.ToPtr(""), + ClientAssertionType: lo.ToPtr(""), + ClientAssertion: lo.ToPtr(""), }).Return(&http.Response{ Body: io.NopCloser(strings.NewReader(`{"code": "oidc-pre-authorize-invalid-client-id"}`)), StatusCode: http.StatusBadRequest, diff --git a/pkg/service/attestation/api.go b/pkg/service/attestation/api.go new file mode 100644 index 000000000..7e65708e0 --- /dev/null +++ b/pkg/service/attestation/api.go @@ -0,0 +1,16 @@ +/* +Copyright Gen Digital Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package attestation + +import "context" + +// ServiceInterface defines an interface for Attestation Service. The task of Attestation Service is to validate and +// confirm the device binding and authentication of the wallet instance. +type ServiceInterface interface { + ValidateClientAttestationJWT(ctx context.Context, clientID, clientAttestationJWT string) error + ValidateClientAttestationPoPJWT(ctx context.Context, clientID, clientAttestationPoPJWT string) error +} diff --git a/pkg/service/attestation/attestation_service.go b/pkg/service/attestation/attestation_service.go new file mode 100644 index 000000000..d20ca0419 --- /dev/null +++ b/pkg/service/attestation/attestation_service.go @@ -0,0 +1,46 @@ +/* +Copyright Gen Digital Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +//go:generate mockgen -destination attestation_service_mocks_test.go -package attestation_test -source=attestation_service.go -mock_names httpClient=MockHTTPClient +package attestation + +import ( + "context" + "net/http" +) + +type httpClient interface { + Do(req *http.Request) (*http.Response, error) +} + +// Config defines configuration for Service. +type Config struct { + HTTPClient httpClient +} + +// Service implements attestation functionality for OAuth 2.0 Attestation-Based Client Authentication. +type Service struct { + httpClient httpClient +} + +// NewService returns a new Service instance. +func NewService(config *Config) *Service { + return &Service{ + httpClient: config.HTTPClient, + } +} + +//nolint:revive +func (s *Service) ValidateClientAttestationJWT(ctx context.Context, clientID, clientAttestationJWT string) error { + // TODO: Validate Client Attestation JWT and check the status of Attestation VC. + return nil +} + +//nolint:revive +func (s *Service) ValidateClientAttestationPoPJWT(ctx context.Context, clientID, clientAttestationPoPJWT string) error { + // TODO: Validate Client Attestation Proof of Possession (PoP) JWT. + return nil +} diff --git a/pkg/service/attestation/attestation_service_test.go b/pkg/service/attestation/attestation_service_test.go new file mode 100644 index 000000000..48ca9f1fc --- /dev/null +++ b/pkg/service/attestation/attestation_service_test.go @@ -0,0 +1,81 @@ +/* +Copyright Gen Digital Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package attestation_test + +import ( + "context" + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + + "github.com/trustbloc/vcs/pkg/service/attestation" +) + +func TestService_ValidateClientAttestationJWT(t *testing.T) { + httpClient := NewMockHTTPClient(gomock.NewController(t)) + + var clientID, clientAttestationJWT string + + tests := []struct { + name string + setup func() + check func(t *testing.T, err error) + }{ + { + name: "success", + setup: func() {}, + check: func(t *testing.T, err error) { + require.NoError(t, err) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.setup() + + svc := attestation.NewService(&attestation.Config{ + HTTPClient: httpClient, + }) + + err := svc.ValidateClientAttestationJWT(context.Background(), clientID, clientAttestationJWT) + tt.check(t, err) + }) + } +} + +func TestService_ValidateClientAttestationPoPJWT(t *testing.T) { + httpClient := NewMockHTTPClient(gomock.NewController(t)) + + var clientID, clientAttestationPoPJWT string + + tests := []struct { + name string + setup func() + check func(t *testing.T, err error) + }{ + { + name: "success", + setup: func() {}, + check: func(t *testing.T, err error) { + require.NoError(t, err) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.setup() + + svc := attestation.NewService(&attestation.Config{ + HTTPClient: httpClient, + }) + + err := svc.ValidateClientAttestationPoPJWT(context.Background(), clientID, clientAttestationPoPJWT) + tt.check(t, err) + }) + } +} diff --git a/pkg/service/oidc4ci/api.go b/pkg/service/oidc4ci/api.go index 56a09f871..bba025eb9 100644 --- a/pkg/service/oidc4ci/api.go +++ b/pkg/service/oidc4ci/api.go @@ -248,12 +248,20 @@ type ServiceInterface interface { code string, flowData *common.WalletInitiatedFlowData, ) (TxID, error) - ExchangeAuthorizationCode(ctx context.Context, opState string) (TxID, error) + ExchangeAuthorizationCode( + ctx context.Context, + opState, + clientID, + clientAssertionType, + clientAssertion string, + ) (TxID, error) ValidatePreAuthorizedCodeRequest( ctx context.Context, - preAuthorizedCode string, - pin string, - clientID string, + preAuthorizedCode, + pin, + clientID, + clientAssertionType, + clientAssertion string, ) (*Transaction, error) PrepareCredential(ctx context.Context, req *PrepareCredential) (*PrepareCredentialResult, error) } diff --git a/pkg/service/oidc4ci/oidc4ci_service.go b/pkg/service/oidc4ci/oidc4ci_service.go index d01c42356..b4c587527 100644 --- a/pkg/service/oidc4ci/oidc4ci_service.go +++ b/pkg/service/oidc4ci/oidc4ci_service.go @@ -4,7 +4,7 @@ Copyright SecureKey Technologies Inc. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ -//go:generate mockgen -destination oidc4ci_service_mocks_test.go -self_package mocks -package oidc4ci_test -source=oidc4ci_service.go -mock_names transactionStore=MockTransactionStore,wellKnownService=MockWellKnownService,eventService=MockEventService,pinGenerator=MockPinGenerator,credentialOfferReferenceStore=MockCredentialOfferReferenceStore,claimDataStore=MockClaimDataStore,profileService=MockProfileService,dataProtector=MockDataProtector,kmsRegistry=MockKMSRegistry,cryptoJWTSigner=MockCryptoJWTSigner,jsonSchemaValidator=MockJSONSchemaValidator +//go:generate mockgen -destination oidc4ci_service_mocks_test.go -self_package mocks -package oidc4ci_test -source=oidc4ci_service.go -mock_names transactionStore=MockTransactionStore,wellKnownService=MockWellKnownService,eventService=MockEventService,pinGenerator=MockPinGenerator,credentialOfferReferenceStore=MockCredentialOfferReferenceStore,claimDataStore=MockClaimDataStore,profileService=MockProfileService,dataProtector=MockDataProtector,kmsRegistry=MockKMSRegistry,cryptoJWTSigner=MockCryptoJWTSigner,jsonSchemaValidator=MockJSONSchemaValidator,attestationService=MockAttestationService package oidc4ci @@ -38,6 +38,8 @@ const ( defaultCtx = "https://www.w3.org/2018/credentials/v1" ) +var _ ServiceInterface = (*Service)(nil) + var logger = log.New("oidc4ci") type pinGenerator interface { @@ -116,6 +118,11 @@ type jsonSchemaValidator interface { Validate(data interface{}, schemaID string, schema []byte) error } +type attestationService interface { + ValidateClientAttestationJWT(ctx context.Context, clientID, clientAttestationJWT string) error + ValidateClientAttestationPoPJWT(ctx context.Context, clientID, clientAttestationPoPJWT string) error +} + // Config holds configuration options and dependencies for Service. type Config struct { TransactionStore transactionStore @@ -133,6 +140,7 @@ type Config struct { KMSRegistry kmsRegistry CryptoJWTSigner cryptoJWTSigner JSONSchemaValidator jsonSchemaValidator + AttestationService attestationService } // Service implements VCS credential interaction API for OIDC credential issuance. @@ -152,6 +160,7 @@ type Service struct { kmsRegistry kmsRegistry cryptoJWTSigner cryptoJWTSigner schemaValidator jsonSchemaValidator + attestationService attestationService } // NewService returns a new Service instance. @@ -172,6 +181,7 @@ func NewService(config *Config) (*Service, error) { kmsRegistry: config.KMSRegistry, cryptoJWTSigner: config.CryptoJWTSigner, schemaValidator: config.JSONSchemaValidator, + attestationService: config.AttestationService, }, nil } @@ -372,9 +382,11 @@ func (s *Service) updateAuthorizationDetails(ctx context.Context, ad *Authorizat func (s *Service) ValidatePreAuthorizedCodeRequest( //nolint:gocognit,nolintlint ctx context.Context, - preAuthorizedCode string, - pin string, - clientID string, + preAuthorizedCode, + pin, + clientID, + clientAssertionType, + clientAssertion string, ) (*Transaction, error) { tx, err := s.store.FindByOpState(ctx, preAuthorizedCode) if err != nil { @@ -391,23 +403,25 @@ func (s *Service) ValidatePreAuthorizedCodeRequest( //nolint:gocognit,nolintlint fmt.Errorf("server expects user pin")) } - if clientID == "" { - var profile *profileapi.Issuer - - profile, err = s.profileService.GetProfile(tx.ProfileID, tx.ProfileVersion) - if err != nil { - if strings.Contains(err.Error(), "not found") { - return nil, resterr.NewCustomError(resterr.ProfileNotFound, err) - } - - return nil, resterr.NewSystemError(resterr.IssuerProfileSvcComponent, "GetProfile", err) + profile, err := s.profileService.GetProfile(tx.ProfileID, tx.ProfileVersion) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return nil, resterr.NewCustomError(resterr.ProfileNotFound, err) } + return nil, resterr.NewSystemError(resterr.IssuerProfileSvcComponent, "GetProfile", err) + } + + if clientID == "" { // check if anonymous access is allowed // profile.OIDCConfig is not required for pre-auth flow, so no specific error for this case. if profile.OIDCConfig != nil && !profile.OIDCConfig.PreAuthorizedGrantAnonymousAccessSupported { return nil, resterr.NewCustomError(resterr.OIDCPreAuthorizeInvalidClientID, fmt.Errorf("issuer does not accept Token Request with a Pre-Authorized Code but without a client_id")) } + } else { + if err = s.AuthenticateClient(ctx, profile, clientID, clientAssertionType, clientAssertion); err != nil { + return nil, resterr.NewCustomError(resterr.OIDCClientAuthenticationFailed, err) + } } newState := TransactionStatePreAuthCodeValidated diff --git a/pkg/service/oidc4ci/oidc4ci_service_authenticate_client.go b/pkg/service/oidc4ci/oidc4ci_service_authenticate_client.go new file mode 100644 index 000000000..88518834a --- /dev/null +++ b/pkg/service/oidc4ci/oidc4ci_service_authenticate_client.go @@ -0,0 +1,62 @@ +/* +Copyright Gen Digital Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package oidc4ci + +import ( + "context" + "errors" + "strings" + + "github.com/samber/lo" + + "github.com/trustbloc/vcs/pkg/profile" + "github.com/trustbloc/vcs/pkg/restapi/resterr" +) + +const ( + attestJWTClientAuthType = "attest_jwt_client_auth" + attestJWTClientAuthJWTCount = 2 +) + +func (s *Service) AuthenticateClient( + ctx context.Context, + profile *profile.Issuer, + clientID, + clientAssertionType, + clientAssertion string) error { + if profile.OIDCConfig == nil || !lo.Contains(profile.OIDCConfig.TokenEndpointAuthMethodsSupported, + attestJWTClientAuthType) { + return nil + } + + if clientID == "" { + return resterr.NewCustomError(resterr.OIDCClientAuthenticationFailed, + errors.New("client_id is required")) + } + + if clientAssertionType != "attest_jwt_client_auth" { + return resterr.NewCustomError(resterr.OIDCClientAuthenticationFailed, + errors.New("only supported client assertion type is attest_jwt_client_auth")) + } + + jwts := strings.Split(clientAssertion, "~") + + if len(jwts) != attestJWTClientAuthJWTCount { + return resterr.NewCustomError(resterr.OIDCClientAuthenticationFailed, + errors.New("invalid client assertion format")) + } + + if err := s.attestationService.ValidateClientAttestationJWT(ctx, clientID, jwts[0]); err != nil { + return resterr.NewCustomError(resterr.OIDCClientAuthenticationFailed, err) + } + + if err := s.attestationService.ValidateClientAttestationPoPJWT(ctx, clientID, jwts[1]); err != nil { + return resterr.NewCustomError(resterr.OIDCClientAuthenticationFailed, err) + } + + return nil +} diff --git a/pkg/service/oidc4ci/oidc4ci_service_authenticate_client_test.go b/pkg/service/oidc4ci/oidc4ci_service_authenticate_client_test.go new file mode 100644 index 000000000..a4bf9b557 --- /dev/null +++ b/pkg/service/oidc4ci/oidc4ci_service_authenticate_client_test.go @@ -0,0 +1,262 @@ +/* +Copyright Gen Digital Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package oidc4ci_test + +import ( + "context" + "errors" + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + + profileapi "github.com/trustbloc/vcs/pkg/profile" + "github.com/trustbloc/vcs/pkg/service/oidc4ci" +) + +func TestService_AuthenticateClient(t *testing.T) { + var ( + attestationService *MockAttestationService + profile *profileapi.Issuer + clientID string + clientAssertionType string + clientAssertion string + ) + + tests := []struct { + name string + setup func() + check func(t *testing.T, err error) + }{ + { + name: "success", + setup: func() { + profile = &profileapi.Issuer{ + OIDCConfig: &profileapi.OIDCConfig{ + TokenEndpointAuthMethodsSupported: []string{"attest_jwt_client_auth"}, + }, + } + + clientID = "client-id" + clientAssertionType = "attest_jwt_client_auth" + clientAssertion = "client-attestation-jwt~client-attestation-pop-jwt" + + attestationService = NewMockAttestationService(gomock.NewController(t)) + + attestationService.EXPECT().ValidateClientAttestationJWT( + gomock.Any(), + clientID, + "client-attestation-jwt", + ).Return(nil) + + attestationService.EXPECT().ValidateClientAttestationPoPJWT(gomock.Any(), + clientID, + "client-attestation-pop-jwt", + ).Return(nil) + }, + check: func(t *testing.T, err error) { + require.NoError(t, err) + }, + }, + { + name: "attest_jwt_client_auth not supported by profile", + setup: func() { + profile = &profileapi.Issuer{ + OIDCConfig: &profileapi.OIDCConfig{ + TokenEndpointAuthMethodsSupported: []string{"none"}, + }, + } + + clientID = "client-id" + clientAssertionType = "attest_jwt_client_auth" + clientAssertion = "client-attestation-jwt~client-attestation-pop-jwt" + + attestationService = NewMockAttestationService(gomock.NewController(t)) + + attestationService.EXPECT().ValidateClientAttestationJWT( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Times(0) + + attestationService.EXPECT().ValidateClientAttestationPoPJWT( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Times(0) + }, + check: func(t *testing.T, err error) { + require.NoError(t, err) + }, + }, + { + name: "missing client_id", + setup: func() { + profile = &profileapi.Issuer{ + OIDCConfig: &profileapi.OIDCConfig{ + TokenEndpointAuthMethodsSupported: []string{"attest_jwt_client_auth"}, + }, + } + + clientID = "" + + attestationService = NewMockAttestationService(gomock.NewController(t)) + + attestationService.EXPECT().ValidateClientAttestationJWT( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Times(0) + + attestationService.EXPECT().ValidateClientAttestationPoPJWT( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Times(0) + }, + check: func(t *testing.T, err error) { + require.ErrorContains(t, err, "client_id is required") + }, + }, + { + name: "invalid client assertion type", + setup: func() { + profile = &profileapi.Issuer{ + OIDCConfig: &profileapi.OIDCConfig{ + TokenEndpointAuthMethodsSupported: []string{"attest_jwt_client_auth"}, + }, + } + + clientID = "client-id" + clientAssertionType = "not_supported_client_assertion_type" + + attestationService = NewMockAttestationService(gomock.NewController(t)) + + attestationService.EXPECT().ValidateClientAttestationJWT( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Times(0) + + attestationService.EXPECT().ValidateClientAttestationPoPJWT( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Times(0) + }, + check: func(t *testing.T, err error) { + require.ErrorContains(t, err, "only supported client assertion type is attest_jwt_client_auth") + }, + }, + { + name: "invalid client assertion format", + setup: func() { + profile = &profileapi.Issuer{ + OIDCConfig: &profileapi.OIDCConfig{ + TokenEndpointAuthMethodsSupported: []string{"attest_jwt_client_auth"}, + }, + } + + clientID = "client-id" + clientAssertionType = "attest_jwt_client_auth" + clientAssertion = "invalid_assertion_format" + + attestationService = NewMockAttestationService(gomock.NewController(t)) + + attestationService.EXPECT().ValidateClientAttestationJWT( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Times(0) + + attestationService.EXPECT().ValidateClientAttestationPoPJWT( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Times(0) + }, + check: func(t *testing.T, err error) { + require.ErrorContains(t, err, "invalid client assertion format") + }, + }, + { + name: "fail to validate client attestation jwt", + setup: func() { + profile = &profileapi.Issuer{ + OIDCConfig: &profileapi.OIDCConfig{ + TokenEndpointAuthMethodsSupported: []string{"attest_jwt_client_auth"}, + }, + } + + clientID = "client-id" + clientAssertionType = "attest_jwt_client_auth" + clientAssertion = "client-attestation-jwt~client-attestation-pop-jwt" + + attestationService = NewMockAttestationService(gomock.NewController(t)) + + attestationService.EXPECT().ValidateClientAttestationJWT( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(errors.New("validate error")) + + attestationService.EXPECT().ValidateClientAttestationPoPJWT( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Times(0) + }, + check: func(t *testing.T, err error) { + require.ErrorContains(t, err, "validate error") + }, + }, + { + name: "fail to validate client attestation pop jwt", + setup: func() { + profile = &profileapi.Issuer{ + OIDCConfig: &profileapi.OIDCConfig{ + TokenEndpointAuthMethodsSupported: []string{"attest_jwt_client_auth"}, + }, + } + + clientID = "client-id" + clientAssertionType = "attest_jwt_client_auth" + clientAssertion = "client-attestation-jwt~client-attestation-pop-jwt" + + attestationService = NewMockAttestationService(gomock.NewController(t)) + + attestationService.EXPECT().ValidateClientAttestationJWT( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(nil) + + attestationService.EXPECT().ValidateClientAttestationPoPJWT( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(errors.New("validate error")) + }, + check: func(t *testing.T, err error) { + require.ErrorContains(t, err, "validate error") + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.setup() + + svc, err := oidc4ci.NewService(&oidc4ci.Config{ + AttestationService: attestationService, + }) + require.NoError(t, err) + + err = svc.AuthenticateClient(context.Background(), profile, clientID, clientAssertionType, clientAssertion) + tt.check(t, err) + }) + } +} diff --git a/pkg/service/oidc4ci/oidc4ci_service_exchange_code.go b/pkg/service/oidc4ci/oidc4ci_service_exchange_code.go index b9f69b31a..dfb3d9cdd 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_exchange_code.go +++ b/pkg/service/oidc4ci/oidc4ci_service_exchange_code.go @@ -17,7 +17,13 @@ import ( "github.com/trustbloc/vcs/pkg/restapi/resterr" ) -func (s *Service) ExchangeAuthorizationCode(ctx context.Context, opState string) (TxID, error) { +func (s *Service) ExchangeAuthorizationCode( + ctx context.Context, + opState, + clientID, + clientAssertionType, + clientAssertion string, +) (TxID, error) { tx, err := s.store.FindByOpState(ctx, opState) if err != nil { return "", fmt.Errorf("get transaction by opstate: %w", err) @@ -45,6 +51,11 @@ func (s *Service) ExchangeAuthorizationCode(ctx context.Context, opState string) return "", e } + if err = s.AuthenticateClient(ctx, profile, clientID, clientAssertionType, clientAssertion); err != nil { + s.sendFailedTransactionEvent(ctx, tx, err) + return "", resterr.NewCustomError(resterr.OIDCClientAuthenticationFailed, err) + } + oauth2Client := oauth2.Config{ ClientID: profile.OIDCConfig.ClientID, ClientSecret: profile.OIDCConfig.ClientSecretHandle, diff --git a/pkg/service/oidc4ci/oidc4ci_service_exchange_code_test.go b/pkg/service/oidc4ci/oidc4ci_service_exchange_code_test.go index 8613b9260..786a9e820 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_exchange_code_test.go +++ b/pkg/service/oidc4ci/oidc4ci_service_exchange_code_test.go @@ -87,7 +87,7 @@ func TestExchangeCode(t *testing.T) { }, }, nil) - resp, err := svc.ExchangeAuthorizationCode(context.TODO(), opState) + resp, err := svc.ExchangeAuthorizationCode(context.TODO(), opState, "", "", "") assert.NoError(t, err) assert.NotEmpty(t, resp) } @@ -98,7 +98,7 @@ func TestExchangeCodeErrFindTx(t *testing.T) { assert.NoError(t, err) store.EXPECT().FindByOpState(gomock.Any(), gomock.Any()).Return(nil, errors.New("tx not found")) - resp, err := svc.ExchangeAuthorizationCode(context.TODO(), "123") + resp, err := svc.ExchangeAuthorizationCode(context.TODO(), "123", "", "", "") assert.Empty(t, resp) assert.ErrorContains(t, err, "tx not found") } @@ -133,11 +133,52 @@ func TestExchangeCodeProfileGetError(t *testing.T) { return nil }) - resp, err := svc.ExchangeAuthorizationCode(context.TODO(), "opState") + resp, err := svc.ExchangeAuthorizationCode(context.TODO(), "opState", "", "", "") assert.Empty(t, resp) assert.ErrorContains(t, err, "get profile error") } +func TestExchangeCodeAuthenticateClientError(t *testing.T) { + store := NewMockTransactionStore(gomock.NewController(t)) + eventMock := NewMockEventService(gomock.NewController(t)) + profileService := NewMockProfileService(gomock.NewController(t)) + attestationService := NewMockAttestationService(gomock.NewController(t)) + + svc, err := oidc4ci.NewService(&oidc4ci.Config{ + TransactionStore: store, + ProfileService: profileService, + AttestationService: attestationService, + EventService: eventMock, + EventTopic: spi.IssuerEventTopic, + }) + assert.NoError(t, err) + + store.EXPECT().FindByOpState(gomock.Any(), gomock.Any()).Return(&oidc4ci.Transaction{ + TransactionData: oidc4ci.TransactionData{ + State: oidc4ci.TransactionStateAwaitingIssuerOIDCAuthorization, + TokenEndpoint: "https://localhost/token", + }, + }, nil) + + profileService.EXPECT().GetProfile(gomock.Any(), gomock.Any()).Return(&profile.Issuer{ + OIDCConfig: &profile.OIDCConfig{ + TokenEndpointAuthMethodsSupported: []string{"attest_jwt_client_auth"}, + }, + }, nil) + + eventMock.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). + DoAndReturn(func(ctx context.Context, topic string, messages ...*spi.Event) error { + assert.Len(t, messages, 1) + assert.Equal(t, messages[0].Type, spi.IssuerOIDCInteractionFailed) + + return nil + }) + + resp, err := svc.ExchangeAuthorizationCode(context.TODO(), "opState", "client_id", "attest_jwt_client_auth", "") + assert.Empty(t, resp) + assert.ErrorContains(t, err, "invalid client assertion format") +} + func TestExchangeCodeIssuerError(t *testing.T) { store := NewMockTransactionStore(gomock.NewController(t)) eventMock := NewMockEventService(gomock.NewController(t)) @@ -186,7 +227,7 @@ func TestExchangeCodeIssuerError(t *testing.T) { }, }, nil) - resp, err := svc.ExchangeAuthorizationCode(context.TODO(), "sadsadas") + resp, err := svc.ExchangeAuthorizationCode(context.TODO(), "sadsadas", "", "", "") assert.Empty(t, resp) assert.ErrorContains(t, err, "oauth2: server response missing access_token") } @@ -247,7 +288,7 @@ func TestExchangeCodeStoreUpdateErr(t *testing.T) { store.EXPECT().FindByOpState(gomock.Any(), opState).Return(baseTx, nil) store.EXPECT().Update(gomock.Any(), gomock.Any()).Return(errors.New("update error")) - resp, err := svc.ExchangeAuthorizationCode(context.TODO(), opState) + resp, err := svc.ExchangeAuthorizationCode(context.TODO(), opState, "", "", "") assert.ErrorContains(t, err, "update error") assert.Empty(t, resp) } @@ -278,7 +319,7 @@ func TestExchangeCodeInvalidState(t *testing.T) { return nil }) - resp, err := svc.ExchangeAuthorizationCode(context.TODO(), "sadsadas") + resp, err := svc.ExchangeAuthorizationCode(context.TODO(), "sadsadas", "", "", "") assert.Empty(t, resp) assert.ErrorContains(t, err, "unexpected transition from 5 to 4") } @@ -346,7 +387,7 @@ func TestExchangeCodePublishError(t *testing.T) { }, }, nil) - resp, err := svc.ExchangeAuthorizationCode(context.TODO(), opState) + resp, err := svc.ExchangeAuthorizationCode(context.TODO(), opState, "", "", "") assert.ErrorContains(t, err, "publish error") assert.Empty(t, resp) } diff --git a/pkg/service/oidc4ci/oidc4ci_service_test.go b/pkg/service/oidc4ci/oidc4ci_service_test.go index 3663d690c..b3b36c04a 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_test.go +++ b/pkg/service/oidc4ci/oidc4ci_service_test.go @@ -845,20 +845,27 @@ func TestPrepareClaimDataAuthorizationForWalletFlow(t *testing.T) { func TestValidatePreAuthCode(t *testing.T) { t.Run("success with pin", func(t *testing.T) { - storeMock := NewMockTransactionStore(gomock.NewController(t)) + profileService := NewMockProfileService(gomock.NewController(t)) eventService := NewMockEventService(gomock.NewController(t)) pinGenerator := NewMockPinGenerator(gomock.NewController(t)) - profileService := NewMockProfileService(gomock.NewController(t)) + storeMock := NewMockTransactionStore(gomock.NewController(t)) srv, err := oidc4ci.NewService(&oidc4ci.Config{ + ProfileService: profileService, TransactionStore: storeMock, EventService: eventService, EventTopic: spi.IssuerEventTopic, PinGenerator: pinGenerator, - ProfileService: profileService, }) assert.NoError(t, err) + profileService.EXPECT().GetProfile(gomock.Any(), gomock.Any()). + Return(&profileapi.Issuer{ + OIDCConfig: &profileapi.OIDCConfig{ + PreAuthorizedGrantAnonymousAccessSupported: true, + }, + }, nil) + pinGenerator.EXPECT().Validate("567", "567").Return(true) storeMock.EXPECT().FindByOpState(gomock.Any(), "1234").Return(&oidc4ci.Transaction{ TransactionData: oidc4ci.TransactionData{ @@ -877,34 +884,27 @@ func TestValidatePreAuthCode(t *testing.T) { return nil }) - profileService.EXPECT().GetProfile(gomock.Any(), gomock.Any()). - Return(&profileapi.Issuer{ - OIDCConfig: &profileapi.OIDCConfig{ - ClientID: "clientID", - ClientSecretHandle: "clientSecret", - PreAuthorizedGrantAnonymousAccessSupported: true, - }, - }, nil) - storeMock.EXPECT().Update(gomock.Any(), gomock.Any()).Return(nil) - resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "567", "") + resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "567", "", "", "") assert.NoError(t, err) assert.NotNil(t, resp) }) t.Run("success without pin", func(t *testing.T) { + profileService := NewMockProfileService(gomock.NewController(t)) storeMock := NewMockTransactionStore(gomock.NewController(t)) eventMock := NewMockEventService(gomock.NewController(t)) - profileService := NewMockProfileService(gomock.NewController(t)) srv, err := oidc4ci.NewService(&oidc4ci.Config{ + ProfileService: profileService, TransactionStore: storeMock, EventService: eventMock, EventTopic: spi.IssuerEventTopic, - ProfileService: profileService, }) assert.NoError(t, err) + profileService.EXPECT().GetProfile(gomock.Any(), gomock.Any()).Return(&profileapi.Issuer{}, nil) + eventMock.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). DoAndReturn(func(ctx context.Context, topic string, messages ...*spi.Event) error { assert.Len(t, messages, 1) @@ -923,22 +923,26 @@ func TestValidatePreAuthCode(t *testing.T) { }, nil) storeMock.EXPECT().Update(gomock.Any(), gomock.Any()).Return(nil) - resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "", "123abc") + resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "", "", "", "") assert.NoError(t, err) assert.NotNil(t, resp) }) t.Run("error with pin during publishing", func(t *testing.T) { + profileService := NewMockProfileService(gomock.NewController(t)) storeMock := NewMockTransactionStore(gomock.NewController(t)) eventMock := NewMockEventService(gomock.NewController(t)) srv, err := oidc4ci.NewService(&oidc4ci.Config{ + ProfileService: profileService, TransactionStore: storeMock, EventService: eventMock, EventTopic: spi.IssuerEventTopic, }) assert.NoError(t, err) + profileService.EXPECT().GetProfile(gomock.Any(), gomock.Any()).Return(&profileapi.Issuer{}, nil) + eventMock.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). DoAndReturn(func(ctx context.Context, topic string, messages ...*spi.Event) error { assert.Len(t, messages, 1) @@ -957,21 +961,25 @@ func TestValidatePreAuthCode(t *testing.T) { }, nil) storeMock.EXPECT().Update(gomock.Any(), gomock.Any()).Return(nil) - resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "", "123abc") + resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "", "", "", "") assert.ErrorContains(t, err, "unexpected error") assert.Nil(t, resp) }) t.Run("invalid pin", func(t *testing.T) { + profileService := NewMockProfileService(gomock.NewController(t)) storeMock := NewMockTransactionStore(gomock.NewController(t)) pinGenerator := NewMockPinGenerator(gomock.NewController(t)) srv, err := oidc4ci.NewService(&oidc4ci.Config{ + ProfileService: profileService, TransactionStore: storeMock, PinGenerator: pinGenerator, }) assert.NoError(t, err) + profileService.EXPECT().GetProfile(gomock.Any(), gomock.Any()).Return(&profileapi.Issuer{}, nil) + pinGenerator.EXPECT().Validate("567", "111").Return(false) storeMock.EXPECT().FindByOpState(gomock.Any(), "1234").Return(&oidc4ci.Transaction{ @@ -983,12 +991,12 @@ func TestValidatePreAuthCode(t *testing.T) { }, }, nil) - resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "111", "123abc") + resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "111", "", "", "") assert.ErrorContains(t, err, "invalid pin") assert.Nil(t, resp) }) - t.Run("fail find tx", func(t *testing.T) { + t.Run("fail to find tx", func(t *testing.T) { storeMock := NewMockTransactionStore(gomock.NewController(t)) srv, err := oidc4ci.NewService(&oidc4ci.Config{ TransactionStore: storeMock, @@ -997,18 +1005,23 @@ func TestValidatePreAuthCode(t *testing.T) { storeMock.EXPECT().FindByOpState(gomock.Any(), gomock.Any()).Return(nil, errors.New("not found")) - resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "", "123abc") + resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "", "", "", "") assert.ErrorContains(t, err, "not found") assert.Nil(t, resp) }) t.Run("invalid state", func(t *testing.T) { + profileService := NewMockProfileService(gomock.NewController(t)) storeMock := NewMockTransactionStore(gomock.NewController(t)) + srv, err := oidc4ci.NewService(&oidc4ci.Config{ + ProfileService: profileService, TransactionStore: storeMock, }) assert.NoError(t, err) + profileService.EXPECT().GetProfile(gomock.Any(), gomock.Any()).Return(&profileapi.Issuer{}, nil) + storeMock.EXPECT().FindByOpState(gomock.Any(), "1234").Return(&oidc4ci.Transaction{ TransactionData: oidc4ci.TransactionData{ PreAuthCode: "1234", @@ -1018,14 +1031,17 @@ func TestValidatePreAuthCode(t *testing.T) { }, }, nil) - resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "567", "123abc") + resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "567", "", "", "") assert.ErrorContains(t, err, "unexpected transition from 5 to 2") assert.Nil(t, resp) }) t.Run("pin should not be provided", func(t *testing.T) { + profileService := NewMockProfileService(gomock.NewController(t)) storeMock := NewMockTransactionStore(gomock.NewController(t)) + srv, err := oidc4ci.NewService(&oidc4ci.Config{ + ProfileService: profileService, TransactionStore: storeMock, }) assert.NoError(t, err) @@ -1038,14 +1054,17 @@ func TestValidatePreAuthCode(t *testing.T) { }, }, nil) - resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "567", "123abc") + resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "567", "", "", "") assert.ErrorContains(t, err, "oidc-pre-authorize-does-not-expect-pin: server does not expect pin") assert.Nil(t, resp) }) t.Run("pin should be provided", func(t *testing.T) { + profileService := NewMockProfileService(gomock.NewController(t)) storeMock := NewMockTransactionStore(gomock.NewController(t)) + srv, err := oidc4ci.NewService(&oidc4ci.Config{ + ProfileService: profileService, TransactionStore: storeMock, }) assert.NoError(t, err) @@ -1059,17 +1078,18 @@ func TestValidatePreAuthCode(t *testing.T) { }, }, nil) - resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "", "123abc") + resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "", "", "", "") assert.ErrorContains(t, err, "oidc-pre-authorize-expect-pin: server expects user pin") assert.Nil(t, resp) }) t.Run("get profile error", func(t *testing.T) { - storeMock := NewMockTransactionStore(gomock.NewController(t)) profileService := NewMockProfileService(gomock.NewController(t)) + storeMock := NewMockTransactionStore(gomock.NewController(t)) + srv, err := oidc4ci.NewService(&oidc4ci.Config{ - TransactionStore: storeMock, ProfileService: profileService, + TransactionStore: storeMock, }) assert.NoError(t, err) @@ -1085,17 +1105,18 @@ func TestValidatePreAuthCode(t *testing.T) { profileService.EXPECT().GetProfile(gomock.Any(), gomock.Any()). Return(nil, errors.New("some error")) - resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "123", "") + resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "123", "", "", "") assert.ErrorContains(t, err, "some error") assert.Nil(t, resp) }) t.Run("issuer does not accept Token Request with a Pre-Authorized Code but without a client_id", func(t *testing.T) { - storeMock := NewMockTransactionStore(gomock.NewController(t)) profileService := NewMockProfileService(gomock.NewController(t)) + storeMock := NewMockTransactionStore(gomock.NewController(t)) + srv, err := oidc4ci.NewService(&oidc4ci.Config{ - TransactionStore: storeMock, ProfileService: profileService, + TransactionStore: storeMock, }) assert.NoError(t, err) @@ -1117,19 +1138,51 @@ func TestValidatePreAuthCode(t *testing.T) { }, }, nil) - resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "123", "") + resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "123", "", "", "") assert.ErrorContains(t, err, "oidc-pre-authorize-invalid-client-id: issuer does not accept "+ "Token Request with a Pre-Authorized Code but without a client_id") assert.Nil(t, resp) }) + t.Run("fail to authenticate client", func(t *testing.T) { + profileService := NewMockProfileService(gomock.NewController(t)) + attestationService := NewMockAttestationService(gomock.NewController(t)) + storeMock := NewMockTransactionStore(gomock.NewController(t)) + + srv, err := oidc4ci.NewService(&oidc4ci.Config{ + ProfileService: profileService, + AttestationService: attestationService, + TransactionStore: storeMock, + }) + assert.NoError(t, err) + + profileService.EXPECT().GetProfile(gomock.Any(), gomock.Any()).Return(&profileapi.Issuer{ + OIDCConfig: &profileapi.OIDCConfig{ + TokenEndpointAuthMethodsSupported: []string{"attest_jwt_client_auth"}, + }, + }, nil) + + storeMock.EXPECT().FindByOpState(gomock.Any(), "1234").Return(&oidc4ci.Transaction{}, nil) + + resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "", "client_id", + "attest_jwt_client_auth", "") + + assert.ErrorContains(t, err, "invalid client assertion format") + assert.Nil(t, resp) + }) + t.Run("valid pre auth code", func(t *testing.T) { + profileService := NewMockProfileService(gomock.NewController(t)) storeMock := NewMockTransactionStore(gomock.NewController(t)) + srv, err := oidc4ci.NewService(&oidc4ci.Config{ + ProfileService: profileService, TransactionStore: storeMock, }) assert.NoError(t, err) + profileService.EXPECT().GetProfile(gomock.Any(), gomock.Any()).Return(&profileapi.Issuer{}, nil) + storeMock.EXPECT().FindByOpState(gomock.Any(), "1234").Return(&oidc4ci.Transaction{ TransactionData: oidc4ci.TransactionData{ PreAuthCode: "12345", @@ -1139,18 +1192,23 @@ func TestValidatePreAuthCode(t *testing.T) { }, }, nil) - resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "123", "123abc") + resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "123", "", "", "") assert.ErrorContains(t, err, "oidc-tx-not-found: invalid pre-authorization code") assert.Nil(t, resp) }) t.Run("error - expired pre auth code", func(t *testing.T) { + profileService := NewMockProfileService(gomock.NewController(t)) storeMock := NewMockTransactionStore(gomock.NewController(t)) + srv, err := oidc4ci.NewService(&oidc4ci.Config{ + ProfileService: profileService, TransactionStore: storeMock, }) assert.NoError(t, err) + profileService.EXPECT().GetProfile(gomock.Any(), gomock.Any()).Return(&profileapi.Issuer{}, nil) + storeMock.EXPECT().FindByOpState(gomock.Any(), "1234").Return(&oidc4ci.Transaction{ TransactionData: oidc4ci.TransactionData{ PreAuthCode: "1234", @@ -1160,18 +1218,23 @@ func TestValidatePreAuthCode(t *testing.T) { }, }, nil) - resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "123", "123abc") + resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "123", "", "", "") assert.ErrorContains(t, err, "oidc-tx-not-found: invalid pre-authorization code") assert.Nil(t, resp) }) t.Run("store update error", func(t *testing.T) { + profileService := NewMockProfileService(gomock.NewController(t)) storeMock := NewMockTransactionStore(gomock.NewController(t)) + srv, err := oidc4ci.NewService(&oidc4ci.Config{ + ProfileService: profileService, TransactionStore: storeMock, }) assert.NoError(t, err) + profileService.EXPECT().GetProfile(gomock.Any(), gomock.Any()).Return(&profileapi.Issuer{}, nil) + storeMock.EXPECT().FindByOpState(gomock.Any(), "1234").Return(&oidc4ci.Transaction{ TransactionData: oidc4ci.TransactionData{ PreAuthCode: "1234", @@ -1182,7 +1245,7 @@ func TestValidatePreAuthCode(t *testing.T) { }, nil) storeMock.EXPECT().Update(gomock.Any(), gomock.Any()).Return(errors.New("store update error")) - resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "", "123abc") + resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "", "", "", "") assert.ErrorContains(t, err, "store update error") assert.Nil(t, resp) })