Skip to content

Commit

Permalink
feat: add verifiable credentials handler
Browse files Browse the repository at this point in the history
  • Loading branch information
hperl committed Jul 24, 2023
1 parent 8098e48 commit 91991bc
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 3 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 @@ -34,6 +34,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 @@ -68,6 +68,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 @@ -360,7 +363,7 @@ func (c *Config) GetAuthorizeCodeLifespan(_ context.Context) time.Duration {
return c.AuthorizeCodeLifespan
}

// 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 @@ -376,6 +379,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 @@ -385,7 +396,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 @@ -106,6 +106,7 @@ type Configurator interface {
RefreshTokenScopesProvider
AccessTokenLifespanProvider
RefreshTokenLifespanProvider
VerifiableCredentialsNonceLifespanProvider
AuthorizeCodeLifespanProvider
TokenEntropyProvider
RotatedGlobalSecretsProvider
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,4 @@ require (
gopkg.in/yaml.v3 v3.0.1 // indirect
)

go 1.17
go 1.20
68 changes: 68 additions & 0 deletions handler/verifiable/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// 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)
}

if !request.GetGrantedScopes().Has("openid") {
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)
}

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

response.SetExtra(draftNonceField, nonce)
response.SetExtra(draftNonceExpField, expiry.Unix())

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(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(_ context.Context, accessToken string, exp time.Time) (string, error) {
assert.Equal(m.t, "fake access token", accessToken)
assert.WithinDuration(m.t, time.Now().Add(time.Hour), exp, 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, expiresIn 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 91991bc

Please sign in to comment.