Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for OIDC VC #758

Merged
merged 2 commits into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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) {
hperl marked this conversation as resolved.
Show resolved Hide resolved
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
}