Skip to content

Commit

Permalink
Merge pull request trustbloc#1518 from aholovko/jwt_client_attestatio…
Browse files Browse the repository at this point in the history
…n_token_req

feat: attestation-based client authentication in the token request
  • Loading branch information
aholovko authored Nov 9, 2023
2 parents 4e4972f + 9366f03 commit c426ea3
Show file tree
Hide file tree
Showing 24 changed files with 951 additions and 255 deletions.
298 changes: 151 additions & 147 deletions api/spec/openapi.gen.go

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions cmd/vc-rest/startcmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down
25 changes: 20 additions & 5 deletions docs/v1/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions pkg/observability/metrics/prometheus/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ func NewMetrics(
metrics.ClientOIDC4CI, metrics.ClientOIDC4CIV1,
metrics.ClientWellKnown, metrics.ClientCredentialVerifier,
metrics.ClientDiscoverableClientIDScheme,
metrics.ClientAttestationService,
}

pm := &PromMetrics{
Expand Down
1 change: 1 addition & 0 deletions pkg/observability/metrics/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions pkg/observability/tracing/wrappers/oidc4ci/oidc4ci_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,19 +70,19 @@ 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()

span.SetAttributes(attribute.String("pre-authorized_code", preAuthorizedCode))
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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,23 +77,23 @@ 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)
}

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)
}

Expand Down
1 change: 1 addition & 0 deletions pkg/restapi/resterr/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
15 changes: 12 additions & 3 deletions pkg/restapi/v1/issuer/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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
}
Expand Down
11 changes: 6 additions & 5 deletions pkg/restapi/v1/issuer/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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{
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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{
Expand Down
18 changes: 16 additions & 2 deletions pkg/restapi/v1/issuer/openapi.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 13 additions & 4 deletions pkg/restapi/v1/oidc4ci/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
13 changes: 9 additions & 4 deletions pkg/restapi/v1/oidc4ci/controller_e2e_flows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit c426ea3

Please sign in to comment.