Skip to content

Commit

Permalink
Auth: feature toggle for enabling v2 Authorization Endpoint (#3287)
Browse files Browse the repository at this point in the history
  • Loading branch information
reinkrul authored Aug 5, 2024
1 parent d973b9a commit 2945144
Show file tree
Hide file tree
Showing 15 changed files with 109 additions and 13 deletions.
2 changes: 2 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ The following options can be configured on the server:
url Public facing URL of the server (required). Must be HTTPS when strictmode is set.
verbosity info Log level (trace, debug, info, warn, error)
httpclient.timeout 30s Request time-out for HTTP clients, such as '10s'. Refer to Golang's 'time.Duration' syntax for a more elaborate description of the syntax.
**Auth**
auth.authorizationendpoint.enabled false enables the v2 API's OAuth2 Authorization Endpoint, used by OpenID4VP and OpenID4VCI. This flag might be removed in a future version (or its default become 'true') as the use cases and implementation of OpenID4VP and OpenID4VCI mature.
**Crypto**
crypto.storage Storage to use, 'fs' for file system (for development purposes), 'vaultkv' for HashiCorp Vault KV store, 'azure-keyvault' for Azure Key Vault, 'external' for an external backend (deprecated).
crypto.azurekv.hsm false Whether to store the key in a hardware security module (HSM). If true, the Azure Key Vault must be configured for HSM usage. Default: false
Expand Down
6 changes: 6 additions & 0 deletions auth/api/auth/v1/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ type TestContext struct {
audit context.Context
}

var _ pkg2.AuthenticationServices = &mockAuthClient{}

type mockAuthClient struct {
ctrl *gomock.Controller
authzServer *oauth.MockAuthorizationServer
Expand All @@ -66,6 +68,10 @@ type mockAuthClient struct {
relyingParty *oauth.MockRelyingParty
}

func (m *mockAuthClient) AuthorizationEndpointEnabled() bool {
return true
}

func (m *mockAuthClient) AuthzServer() oauth.AuthorizationServer {
return m.authzServer
}
Expand Down
18 changes: 16 additions & 2 deletions auth/api/iam/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,6 @@ type httpRequestContextKey struct{}
// TODO: Might want to make this configurable at some point
const accessTokenValidity = 15 * time.Minute

const oid4vciSessionValidity = 15 * time.Minute

// cacheControlMaxAgeURLs holds API endpoints that should have a max-age cache control header set.
var cacheControlMaxAgeURLs = []string{
"/oauth2/:did/presentation_definition",
Expand Down Expand Up @@ -241,6 +239,13 @@ func (r Wrapper) HandleTokenRequest(ctx context.Context, request HandleTokenRequ
}

func (r Wrapper) Callback(ctx context.Context, request CallbackRequestObject) (CallbackResponseObject, error) {
if !r.auth.AuthorizationEndpointEnabled() {
// Callback endpoint is only used by flows initiated through the authorization endpoint.
return nil, oauth.OAuth2Error{
Code: oauth.InvalidRequest,
Description: "callback endpoint is disabled",
}
}
// validate request
// check did in path
ownDID, err := r.toOwnedDID(ctx, request.Did)
Expand Down Expand Up @@ -419,6 +424,12 @@ func (r Wrapper) introspectAccessToken(input string) (*ExtendedTokenIntrospectio

// HandleAuthorizeRequest handles calls to the authorization endpoint for starting an authorization code flow.
func (r Wrapper) HandleAuthorizeRequest(ctx context.Context, request HandleAuthorizeRequestRequestObject) (HandleAuthorizeRequestResponseObject, error) {
if !r.auth.AuthorizationEndpointEnabled() {
return nil, oauth.OAuth2Error{
Code: oauth.InvalidRequest,
Description: "authorization endpoint is disabled",
}
}
ownDID, err := r.toOwnedDIDForOAuth2(ctx, request.Did)
if err != nil {
return nil, err
Expand Down Expand Up @@ -601,6 +612,9 @@ func (r Wrapper) oauthAuthorizationServerMetadata(ownDID *did.DID) (*oauth.Autho
return nil, err
}
md := authorizationServerMetadata(*ownDID, issuerURL)
if !r.auth.AuthorizationEndpointEnabled() {
md.AuthorizationEndpoint = ""
}
return &md, nil
}

Expand Down
35 changes: 30 additions & 5 deletions auth/api/iam/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,23 @@ func TestWrapper_OAuthAuthorizationServerMetadata(t *testing.T) {

require.NoError(t, err)
assert.IsType(t, OAuthAuthorizationServerMetadata200JSONResponse{}, res)
assert.NotEmpty(t, res.(OAuthAuthorizationServerMetadata200JSONResponse).AuthorizationEndpoint)
})
t.Run("authorization endpoint disabled", func(t *testing.T) {
ctx := newCustomTestClient(t, verifierURL, false)
ctx.documentOwner.EXPECT().IsOwner(nil, webDID).Return(true, nil)

res, err := ctx.client.OAuthAuthorizationServerMetadata(nil, OAuthAuthorizationServerMetadataRequestObject{Did: webDID.String()})

require.NoError(t, err)
assert.IsType(t, OAuthAuthorizationServerMetadata200JSONResponse{}, res)
assert.Empty(t, res.(OAuthAuthorizationServerMetadata200JSONResponse).AuthorizationEndpoint)
})
t.Run("base URL (prepended before /iam)", func(t *testing.T) {
var webDID = did.MustParseDID("did:web:example.com:base:iam:123")
// 200
baseURL := test.MustParseURL("https://example.com/base")
ctx := newTestClientWithBaseURL(t, baseURL)
ctx := newCustomTestClient(t, baseURL, false)
ctx.documentOwner.EXPECT().IsOwner(nil, webDID).Return(true, nil)

res, err := ctx.client.OAuthAuthorizationServerMetadata(nil, OAuthAuthorizationServerMetadataRequestObject{Did: webDID.String()})
Expand Down Expand Up @@ -221,7 +231,14 @@ func TestWrapper_PresentationDefinition(t *testing.T) {
}

func TestWrapper_HandleAuthorizeRequest(t *testing.T) {
t.Run("disabled", func(t *testing.T) {
ctx := newCustomTestClient(t, verifierURL, false)

response, err := ctx.client.HandleAuthorizeRequest(nil, HandleAuthorizeRequestRequestObject{Did: verifierDID.String()})

requireOAuthError(t, err, oauth.InvalidRequest, "authorization endpoint is disabled")
assert.Nil(t, response)
})
t.Run("ok - response_type=code", func(t *testing.T) {
ctx := newTestClient(t)

Expand All @@ -238,7 +255,7 @@ func TestWrapper_HandleAuthorizeRequest(t *testing.T) {
oauth.CodeChallengeParam: "code_challenge",
oauth.CodeChallengeMethodParam: "S256",
}
ctx.documentOwner.EXPECT().IsOwner(gomock.Any(), verifierDID).Return(true, nil).MinTimes(1)
ctx.documentOwner.EXPECT().IsOwner(gomock.Any(), verifierDID).Return(true, nil)
ctx.jar.EXPECT().Parse(gomock.Any(), gomock.Any(), url.Values{"key": []string{"test_value"}}).Return(requestParams, nil)

// handleAuthorizeRequestFromHolder
Expand Down Expand Up @@ -333,7 +350,7 @@ func TestWrapper_HandleAuthorizeRequest(t *testing.T) {
oauth.ResponseTypeParam: "unsupported",
}
ctx.jar.EXPECT().Parse(gomock.Any(), gomock.Any(), gomock.Any()).Return(requestParams, nil)
ctx.documentOwner.EXPECT().IsOwner(gomock.Any(), verifierDID).Return(true, nil).MinTimes(1)
ctx.documentOwner.EXPECT().IsOwner(gomock.Any(), verifierDID).Return(true, nil)

res, err := ctx.client.HandleAuthorizeRequest(requestContext(map[string]interface{}{}),
HandleAuthorizeRequestRequestObject{Did: verifierDID.String()})
Expand Down Expand Up @@ -377,7 +394,14 @@ func TestWrapper_Callback(t *testing.T) {
OtherDID: &verifierDID,
TokenEndpoint: "https://example.com/token",
}
t.Run("disabled", func(t *testing.T) {
ctx := newCustomTestClient(t, verifierURL, false)

response, err := ctx.client.Callback(nil, CallbackRequestObject{Did: holderDID.String()})

requireOAuthError(t, err, oauth.InvalidRequest, "callback endpoint is disabled")
assert.Nil(t, response)
})
t.Run("ok - error flow", func(t *testing.T) {
ctx := newTestClient(t)
ctx.documentOwner.EXPECT().IsOwner(gomock.Any(), holderDID).Return(true, nil)
Expand Down Expand Up @@ -1449,10 +1473,10 @@ type testCtx struct {

func newTestClient(t testing.TB) *testCtx {
publicURL, _ := url.Parse("https://example.com")
return newTestClientWithBaseURL(t, publicURL)
return newCustomTestClient(t, publicURL, true)
}

func newTestClientWithBaseURL(t testing.TB, publicURL *url.URL) *testCtx {
func newCustomTestClient(t testing.TB, publicURL *url.URL, authEndpointEnabled bool) *testCtx {
ctrl := gomock.NewController(t)
storageEngine := storage.NewTestStorageEngine(t)
authnServices := auth.NewMockAuthenticationServices(ctrl)
Expand All @@ -1476,6 +1500,7 @@ func newTestClientWithBaseURL(t testing.TB, publicURL *url.URL) *testCtx {
mockVCR.EXPECT().Verifier().Return(vcVerifier).AnyTimes()
mockVCR.EXPECT().Wallet().Return(mockWallet).AnyTimes()
authnServices.EXPECT().IAMClient().Return(iamClient).AnyTimes()
authnServices.EXPECT().AuthorizationEndpointEnabled().Return(authEndpointEnabled).AnyTimes()
mockVDR.EXPECT().Resolver().Return(mockResolver).AnyTimes()
mockVDR.EXPECT().DocumentOwner().Return(mockDocumentOwner).AnyTimes()

Expand Down
5 changes: 5 additions & 0 deletions auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ func (auth *Auth) PublicURL() *url.URL {
return auth.publicURL
}

// AuthorizationEndpointEnabled returns whether the v2 API's OAuth2 Authorization Endpoint is enabled.
func (auth *Auth) AuthorizationEndpointEnabled() bool {
return auth.config.AuthorizationEndpoint.Enabled
}

// ContractNotary returns an implementation of the ContractNotary interface.
func (auth *Auth) ContractNotary() services.ContractNotary {
return auth.contractNotary
Expand Down
5 changes: 5 additions & 0 deletions auth/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ const ConfHTTPTimeout = "auth.http.timeout"
// ConfAccessTokenLifeSpan defines how long (in seconds) an access token is valid
const ConfAccessTokenLifeSpan = "auth.accesstokenlifespan"

// ConfAuthEndpointEnabled is the config key for enabling the Auth v2 API's Authorization Endpoint
const ConfAuthEndpointEnabled = "auth.authorizationendpoint.enabled"

// FlagSet returns the configuration flags supported by this module.
func FlagSet() *pflag.FlagSet {
flags := pflag.NewFlagSet("auth", pflag.ContinueOnError)
Expand All @@ -52,6 +55,8 @@ func FlagSet() *pflag.FlagSet {
flags.Int(ConfClockSkew, defs.ClockSkew, "allowed JWT Clock skew in milliseconds")
flags.Int(ConfAccessTokenLifeSpan, defs.AccessTokenLifeSpan, "defines how long (in seconds) an access token is valid. Uses default in strict mode.")
flags.StringSlice(ConfContractValidators, defs.ContractValidators, "sets the different contract validators to use")
flags.Bool(ConfAuthEndpointEnabled, defs.AuthorizationEndpoint.Enabled, "enables the v2 API's OAuth2 Authorization Endpoint, used by OpenID4VP and OpenID4VCI. "+
"This flag might be removed in a future version (or its default become 'true') as the use cases and implementation of OpenID4VP and OpenID4VCI mature.")
_ = flags.MarkDeprecated("auth.http.timeout", "use httpclient.timeout instead")

return flags
Expand Down
1 change: 1 addition & 0 deletions auth/cmd/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func TestFlagSet(t *testing.T) {

assert.Equal(t, []string{
ConfAccessTokenLifeSpan,
ConfAuthEndpointEnabled,
ConfClockSkew,
ConfContractValidators,
ConfHTTPTimeout,
Expand Down
20 changes: 15 additions & 5 deletions auth/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,21 @@ import (

// Config holds all the configuration params
type Config struct {
Irma IrmaConfig `koanf:"irma"`
HTTPTimeout int `koanf:"http.timeout"`
ClockSkew int `koanf:"clockskew"`
ContractValidators []string `koanf:"contractvalidators"`
AccessTokenLifeSpan int `koanf:"accesstokenlifespan"`
Irma IrmaConfig `koanf:"irma"`
HTTPTimeout int `koanf:"http.timeout"`
ClockSkew int `koanf:"clockskew"`
ContractValidators []string `koanf:"contractvalidators"`
AccessTokenLifeSpan int `koanf:"accesstokenlifespan"`
AuthorizationEndpoint AuthorizationEndpointConfig `koanf:"authorizationendpoint"`
}

type AuthorizationEndpointConfig struct {
// Enabled is a flag to enable or disable the v2 API's Authorization Endpoint (/authorize), used for:
// - As OpenID4VP verifier: to authenticate clients (that initiate the Authorized Code flow) using OpenID4VP
// - As OpenID4VP wallet: to authenticate verifiers using OpenID4VP
// - As OpenID4VCI wallet: to support dynamic credential requests (currently not supported)
// Disabling the authorization endpoint will also disable to callback endpoint and removes the endpoint from the metadata.
Enabled bool `koanf:"enabled"`
}

type IrmaConfig struct {
Expand Down
2 changes: 2 additions & 0 deletions auth/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,6 @@ type AuthenticationServices interface {
ContractNotary() services.ContractNotary
// PublicURL returns the public URL of the node.
PublicURL() *url.URL
// AuthorizationEndpointEnabled returns whether the v2 API's OAuth2 Authorization Endpoint is enabled.
AuthorizationEndpointEnabled() bool
}
14 changes: 14 additions & 0 deletions auth/mock.go

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

6 changes: 5 additions & 1 deletion docs/generate_docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,11 @@ func generateServerOptions(system *core.System) {
return strings.HasPrefix(f.Name, "events.")
},
func(f *pflag.Flag) bool {
return strings.HasPrefix(f.Name, "auth.")
// Auth engine
return strings.HasPrefix(f.Name, "auth.irma") ||
strings.HasPrefix(f.Name, "auth.clockskew") ||
strings.HasPrefix(f.Name, "auth.contractvalidators") ||
strings.HasPrefix(f.Name, "auth.accesstokenlifespan")
},
func(f *pflag.Flag) bool {
return strings.HasPrefix(f.Name, "tls.")
Expand Down
Loading

0 comments on commit 2945144

Please sign in to comment.