Skip to content

Commit

Permalink
feat: add verifiable credentials handler (ory#758)
Browse files Browse the repository at this point in the history
  • Loading branch information
hperl authored and shipperizer committed Jan 3, 2024
1 parent 843b159 commit d5dac14
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 2 deletions.
18 changes: 18 additions & 0 deletions compose/compose_userinfo_vc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package compose

import (
"github.com/ory/fosite"
"github.com/ory/fosite/handler/verifiable"
)

// OIDCUserinfoVerifiableCredentialFactory creates a verifiable credentials
// handler.
func OIDCUserinfoVerifiableCredentialFactory(config fosite.Configurator, storage, strategy any) any {
return &verifiable.Handler{
NonceManager: storage.(verifiable.NonceManager),
Config: config,
}
}
6 changes: 6 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ type AccessTokenLifespanProvider interface {
GetAccessTokenLifespan(ctx context.Context) time.Duration
}

// VerifiableCredentialsNonceLifespanProvider returns the provider for configuring the access token lifespan.
type VerifiableCredentialsNonceLifespanProvider interface {
// GetNonceLifespan returns the nonce lifespan.
GetVerifiableCredentialsNonceLifespan(ctx context.Context) time.Duration
}

// IDTokenLifespanProvider returns the provider for configuring the ID token lifespan.
type IDTokenLifespanProvider interface {
// GetIDTokenLifespan returns the ID token lifespan.
Expand Down
15 changes: 13 additions & 2 deletions config_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ type Config struct {
// AccessTokenLifespan sets how long an access token is going to be valid. Defaults to one hour.
AccessTokenLifespan time.Duration

// VerifiableCredentialsNonceLifespan sets how long a verifiable credentials nonce is going to be valid. Defaults to one hour.
VerifiableCredentialsNonceLifespan time.Duration

// RefreshTokenLifespan sets how long a refresh token is going to be valid. Defaults to 30 days. Set to -1 for
// refresh tokens that never expire.
RefreshTokenLifespan time.Duration
Expand Down Expand Up @@ -394,7 +397,7 @@ func (c *Config) GetDeviceAndUserCodeLifespan(_ context.Context) time.Duration {
return c.DeviceAndUserCodeLifespan
}

// GeIDTokenLifespan returns how long an id token should be valid. Defaults to one hour.
// GetIDTokenLifespan returns how long an id token should be valid. Defaults to one hour.
func (c *Config) GetIDTokenLifespan(_ context.Context) time.Duration {
if c.IDTokenLifespan == 0 {
return time.Hour
Expand All @@ -410,6 +413,14 @@ func (c *Config) GetAccessTokenLifespan(_ context.Context) time.Duration {
return c.AccessTokenLifespan
}

// GetNonceLifespan returns how long a nonce should be valid. Defaults to one hour.
func (c *Config) GetVerifiableCredentialsNonceLifespan(_ context.Context) time.Duration {
if c.VerifiableCredentialsNonceLifespan == 0 {
return time.Hour
}
return c.VerifiableCredentialsNonceLifespan
}

// GetRefreshTokenLifespan sets how long a refresh token is going to be valid. Defaults to 30 days. Set to -1 for
// refresh tokens that never expire.
func (c *Config) GetRefreshTokenLifespan(_ context.Context) time.Duration {
Expand All @@ -419,7 +430,7 @@ func (c *Config) GetRefreshTokenLifespan(_ context.Context) time.Duration {
return c.RefreshTokenLifespan
}

// GetHashCost returns the bcrypt cost factor. Defaults to 12.
// GetBCryptCost returns the bcrypt cost factor. Defaults to 12.
func (c *Config) GetBCryptCost(_ context.Context) int {
if c.HashCost == 0 {
return DefaultBCryptWorkFactor
Expand Down
1 change: 1 addition & 0 deletions fosite.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ type Configurator interface {
RefreshTokenScopesProvider
AccessTokenLifespanProvider
RefreshTokenLifespanProvider
VerifiableCredentialsNonceLifespanProvider
AuthorizeCodeLifespanProvider
DeviceAndUserCodeLifespanProvider
TokenEntropyProvider
Expand Down
65 changes: 65 additions & 0 deletions handler/verifiable/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package verifiable

import (
"context"
"time"

"github.com/ory/fosite"
"github.com/ory/x/errorsx"
)

const (
draftScope = "userinfo_credential_draft_00"
draftNonceField = "c_nonce_draft_00"
draftNonceExpField = "c_nonce_expires_in_draft_00"
)

type Handler struct {
Config interface {
fosite.VerifiableCredentialsNonceLifespanProvider
}
NonceManager
}

var _ fosite.TokenEndpointHandler = (*Handler)(nil)

func (c *Handler) HandleTokenEndpointRequest(ctx context.Context, request fosite.AccessRequester) error {
if !c.CanHandleTokenEndpointRequest(ctx, request) {
return errorsx.WithStack(fosite.ErrUnknownRequest)
}

return nil
}

func (c *Handler) PopulateTokenEndpointResponse(
ctx context.Context,
request fosite.AccessRequester,
response fosite.AccessResponder,
) error {
if !c.CanHandleTokenEndpointRequest(ctx, request) {
return errorsx.WithStack(fosite.ErrUnknownRequest)
}

lifespan := c.Config.GetVerifiableCredentialsNonceLifespan(ctx)
expiry := time.Now().UTC().Add(lifespan)
nonce, err := c.NewNonce(ctx, response.GetAccessToken(), expiry)
if err != nil {
return err
}

response.SetExtra(draftNonceField, nonce)
response.SetExtra(draftNonceExpField, int64(lifespan.Seconds()))

return nil
}

func (c *Handler) CanSkipClientAuth(context.Context, fosite.AccessRequester) bool {
return false
}

func (c *Handler) CanHandleTokenEndpointRequest(_ context.Context, requester fosite.AccessRequester) bool {
return requester.GetGrantedScopes().Has("openid", draftScope)
}
73 changes: 73 additions & 0 deletions handler/verifiable/handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package verifiable

import (
"context"
"testing"
"time"

"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"

"github.com/ory/fosite"
"github.com/ory/fosite/internal"
)

type mockNonceManager struct{ t *testing.T }

func (m *mockNonceManager) NewNonce(ctx context.Context, accessToken string, expiresAt time.Time) (string, error) {
assert.Equal(m.t, "fake access token", accessToken)
assert.WithinDuration(m.t, time.Now().Add(time.Hour), expiresAt, 5*time.Second)
return "mocked nonce", nil
}

func (m *mockNonceManager) IsNonceValid(context.Context, string, string) error {
return nil
}

func TestHandler(t *testing.T) {
t.Parallel()
ctx := context.Background()

t.Run("case=correct scopes", func(t *testing.T) {
t.Parallel()
handler := newHandler(t)
ctrl := gomock.NewController(t)
defer ctrl.Finish()

req := internal.NewMockAccessRequester(ctrl)
req.EXPECT().GetGrantedScopes().Return(fosite.Arguments{"openid", draftScope}).AnyTimes()

resp := internal.NewMockAccessResponder(ctrl)
resp.EXPECT().GetAccessToken().Return("fake access token")
resp.EXPECT().SetExtra(gomock.Eq(draftNonceField), gomock.Eq("mocked nonce"))
resp.EXPECT().SetExtra(gomock.Eq(draftNonceExpField), gomock.Any())

assert.NoError(t, handler.HandleTokenEndpointRequest(ctx, req))
assert.NoError(t, handler.PopulateTokenEndpointResponse(ctx, req, resp))
})

t.Run("case=incorrect scopes", func(t *testing.T) {
t.Parallel()
handler := newHandler(t)
ctrl := gomock.NewController(t)
defer ctrl.Finish()

req := internal.NewMockAccessRequester(ctrl)
req.EXPECT().GetGrantedScopes().Return(fosite.Arguments{"openid"}).AnyTimes()

resp := internal.NewMockAccessResponder(ctrl)

assert.ErrorIs(t, handler.HandleTokenEndpointRequest(ctx, req), fosite.ErrUnknownRequest)
assert.ErrorIs(t, handler.PopulateTokenEndpointResponse(ctx, req, resp), fosite.ErrUnknownRequest)
})
}

func newHandler(t *testing.T) *Handler {
return &Handler{
Config: new(fosite.Config),
NonceManager: &mockNonceManager{t: t},
}
}
17 changes: 17 additions & 0 deletions handler/verifiable/nonce.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package verifiable

import (
"context"
"time"
)

type NonceManager interface {
// NewNonce creates a new nonce bound to the access token valid until the given expiry time.
NewNonce(ctx context.Context, accessToken string, expiresAt time.Time) (string, error)

// IsNonceValid checks if the given nonce is valid for the given access token and not expired.
IsNonceValid(ctx context.Context, accessToken string, nonce string) error
}

0 comments on commit d5dac14

Please sign in to comment.