Skip to content
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
6 changes: 2 additions & 4 deletions auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ type Auth struct {
shutdownFunc func()
vdrInstance vdr.VDR
publicURL *url.URL
strictMode bool
httpClientTimeout time.Duration
tlsConfig *tls.Config
subjectManager didsubject.Manager
Expand Down Expand Up @@ -139,7 +138,6 @@ func (auth *Auth) IAMClient() iam.Client {
JWTSigner: auth.keyStore,
LDDocumentLoader: auth.jsonldManager.DocumentLoader(),
PolicyBackend: auth.policyBackend,
StrictMode: auth.strictMode,
HTTPClientTimeout: auth.httpClientTimeout,
ExperimentalJwtBearerClient: auth.config.Experimental.JwtBearerClient,
})
Expand Down Expand Up @@ -194,13 +192,13 @@ func (auth *Auth) Configure(config core.ServerConfig) error {
// auth.http.config got deprecated in favor of httpclient.timeout
auth.httpClientTimeout = config.HTTPClient.Timeout
}
auth.openID4VCIClient = openid4vci.NewClient(httpclient.NewWithCache(auth.httpClientTimeout), auth.strictMode)
auth.openID4VCIClient = openid4vci.NewClient(httpclient.NewWithCache(auth.httpClientTimeout))
// V1 API related stuff
accessTokenLifeSpan := time.Duration(auth.config.AccessTokenLifeSpan) * time.Second
auth.authzServer = oauth.NewAuthorizationServer(auth.vdrInstance.Resolver(), auth.vcr, auth.vcr.Verifier(), auth.serviceResolver,
auth.keyStore, auth.contractNotary, auth.jsonldManager, accessTokenLifeSpan)
auth.relyingParty = oauth.NewRelyingParty(auth.vdrInstance.Resolver(), auth.serviceResolver,
auth.keyStore, auth.vcr.Wallet(), auth.httpClientTimeout, auth.tlsConfig, config.Strictmode, auth.pkiProvider)
auth.keyStore, auth.vcr.Wallet(), auth.httpClientTimeout, auth.tlsConfig, auth.pkiProvider)

if err := auth.authzServer.Configure(auth.config.ClockSkew, config.Strictmode); err != nil {
return err
Expand Down
10 changes: 2 additions & 8 deletions auth/client/iam/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ var ErrBadGateway = errors.New("upstream returned unexpected result")

// HTTPClient holds the server address and other basic settings for the http client
type HTTPClient struct {
strictMode bool
keyResolver resolver.KeyResolver
httpClient core.HTTPRequestDoer
}
Expand All @@ -70,7 +69,7 @@ func (hb HTTPClient) OAuthAuthorizationServerMetadata(ctx context.Context, oauth
// 1. insert (RFC 8414): https://host/.well-known/oauth-authorization-server/<path>
// 2. append (OIDC Disc): https://host/<path>/.well-known/oauth-authorization-server
// Many authorization servers publish metadata only under the append convention.
metadata, err := oauth.FetchMetadata[oauth.AuthorizationServerMetadata](ctx, hb.httpClient, oauthIssuer, hb.strictMode)
metadata, err := oauth.FetchMetadata[oauth.AuthorizationServerMetadata](ctx, hb.httpClient, oauthIssuer)
if err != nil && errors.Is(err, oauth.ErrAllCandidates4xx) {
// Every candidate rejected the request outright (no 5xx, no network/decode failure, no
// identifier mismatch): the identifier itself is most likely wrong.
Expand All @@ -82,11 +81,6 @@ func (hb HTTPClient) OAuthAuthorizationServerMetadata(ctx context.Context, oauth
// ClientMetadata retrieves the client metadata from the client metadata endpoint given in the authorization request.
// We use the AuthorizationServerMetadata struct since it overlaps greatly with the client metadata.
func (hb HTTPClient) ClientMetadata(ctx context.Context, endpoint string) (*oauth.OAuthClientMetadata, error) {
_, err := core.ParsePublicURL(endpoint, hb.strictMode)
if err != nil {
return nil, err
}

// create a GET request
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
Expand Down Expand Up @@ -237,7 +231,7 @@ func (hb HTTPClient) PostAuthorizationResponse(ctx context.Context, vp vc.Verifi
}

func (hb HTTPClient) OpenIDConfiguration(ctx context.Context, issuerURL string) (*oauth.OpenIDConfiguration, error) {
metadataURL, err := oauth.IssuerIdToWellKnown(issuerURL, oauth.OpenIdConfigurationWellKnown, hb.strictMode)
metadataURL, err := oauth.IssuerIdToWellKnown(issuerURL, oauth.OpenIdConfigurationWellKnown)
if err != nil {
return nil, err
}
Expand Down
4 changes: 2 additions & 2 deletions auth/client/iam/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ func TestHTTPClient_OAuthAuthorizationServerMetadata(t *testing.T) {

// The insert/append fallback, identifier-match, and error-joining behavior is exhaustively
// covered by oauth.FetchMetadata's own tests; this wraps it with no extra logic, so these
// tests only need to confirm the wiring (well-known constant, httpClient, strictMode) and
// that this specific bug (rewriting an upstream status code) doesn't reappear.
// tests only need to confirm the wiring (well-known constant, httpClient) and that this
// specific bug (rewriting an upstream status code) doesn't reappear.
t.Run("ok - append form when insert 404s", func(t *testing.T) {
tlsServer, client, requested := metadataServer(t, http.StatusNotFound, "/iam/123", "/iam/123/.well-known/oauth-authorization-server")

Expand Down
31 changes: 8 additions & 23 deletions auth/client/iam/openid4vp.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import (
"github.com/nuts-foundation/go-did/vc"
"github.com/nuts-foundation/nuts-node/auth/log"
"github.com/nuts-foundation/nuts-node/auth/oauth"
"github.com/nuts-foundation/nuts-node/core"
nutsCrypto "github.com/nuts-foundation/nuts-node/crypto"
"github.com/nuts-foundation/nuts-node/crypto/dpop"
nutsHttp "github.com/nuts-foundation/nuts-node/http"
Expand All @@ -62,7 +61,6 @@ type OpenID4VPClient struct {
httpClient HTTPClient
jwtSigner nutsCrypto.JWTSigner
keyResolver resolver.KeyResolver
strictMode bool
wallet holder.Wallet
ldDocumentLoader ld.DocumentLoader
subjectManager didsubject.Manager
Expand All @@ -74,16 +72,15 @@ type OpenID4VPClient struct {
// ClientConfig groups the dependencies and toggles needed to construct an OpenID4VPClient.
// The interface fields (Wallet, KeyResolver, SubjectManager, JWTSigner, LDDocumentLoader,
// PolicyBackend) are required; NewClient does not validate them and missing fields will surface as
// nil-pointer panics on first use. Scalar fields default to their zero value (StrictMode=false,
// HTTPClientTimeout=0, ExperimentalJwtBearerClient=false) and the zero values are valid.
// nil-pointer panics on first use. Scalar fields default to their zero value (HTTPClientTimeout=0,
// ExperimentalJwtBearerClient=false) and the zero values are valid.
type ClientConfig struct {
Wallet holder.Wallet
KeyResolver resolver.KeyResolver
SubjectManager didsubject.Manager
JWTSigner nutsCrypto.JWTSigner
LDDocumentLoader ld.DocumentLoader
PolicyBackend policy.PDPBackend
StrictMode bool
HTTPClientTimeout time.Duration
// ExperimentalJwtBearerClient gates the RFC 7523 jwt-bearer two-VP token request flow.
// Tracked for replacement by a list-style grant-types config under issue #4231.
Expand All @@ -93,7 +90,6 @@ type ClientConfig struct {
// NewClient returns an OpenID4VPClient configured with the given dependencies.
func NewClient(cfg ClientConfig) *OpenID4VPClient {
httpClient := HTTPClient{
strictMode: cfg.StrictMode,
httpClient: client.NewWithCache(cfg.HTTPClientTimeout),
keyResolver: cfg.KeyResolver,
}
Expand All @@ -103,7 +99,6 @@ func NewClient(cfg ClientConfig) *OpenID4VPClient {
jwtSigner: cfg.JWTSigner,
ldDocumentLoader: cfg.LDDocumentLoader,
subjectManager: cfg.SubjectManager,
strictMode: cfg.StrictMode,
wallet: cfg.Wallet,
policyBackend: cfg.PolicyBackend,
experimentalJwtBearerClient: cfg.ExperimentalJwtBearerClient,
Expand All @@ -128,7 +123,7 @@ func (c *OpenID4VPClient) ClientMetadata(ctx context.Context, endpoint string) (
func (c *OpenID4VPClient) PostError(ctx context.Context, auth2Error oauth.OAuth2Error, verifierResponseURI string, verifierClientState string) (string, error) {
iamClient := c.httpClient

responseURL, err := core.ParsePublicURL(verifierResponseURI, c.strictMode)
responseURL, err := url.Parse(verifierResponseURI)
if err != nil {
return "", fmt.Errorf("failed to post error to verifier: %w", err)
}
Expand All @@ -149,7 +144,7 @@ func (c *OpenID4VPClient) PostError(ctx context.Context, auth2Error oauth.OAuth2
func (c *OpenID4VPClient) PostAuthorizationResponse(ctx context.Context, vp vc.VerifiablePresentation, presentationSubmission pe.PresentationSubmission, verifierResponseURI string, state string) (string, error) {
iamClient := c.httpClient

responseURL, err := core.ParsePublicURL(verifierResponseURI, c.strictMode)
responseURL, err := url.Parse(verifierResponseURI)
if err != nil {
return "", fmt.Errorf("failed to post error to verifier: %w", err)
}
Expand All @@ -163,7 +158,7 @@ func (c *OpenID4VPClient) PostAuthorizationResponse(ctx context.Context, vp vc.V

func (c *OpenID4VPClient) PresentationDefinition(ctx context.Context, endpoint string) (*pe.PresentationDefinition, error) {
iamClient := c.httpClient
parsedURL, err := core.ParsePublicURL(endpoint, c.strictMode)
parsedURL, err := url.Parse(endpoint)
if err != nil {
return nil, fmt.Errorf("failed to retrieve presentation definition: %w", err)
}
Expand Down Expand Up @@ -196,28 +191,19 @@ func (c *OpenID4VPClient) OpenIDConfiguration(ctx context.Context, issuer string

func (c *OpenID4VPClient) RequestObjectByGet(ctx context.Context, requestURI string) (string, error) {
iamClient := c.httpClient
parsedURL, err := core.ParsePublicURL(requestURI, c.strictMode)
if err != nil {
return "", fmt.Errorf("invalid request_uri: %w", err)
}

requestObject, err := iamClient.RequestObjectByGet(ctx, parsedURL.String())
requestObject, err := iamClient.RequestObjectByGet(ctx, requestURI)
if err != nil {
return "", fmt.Errorf("failed to retrieve JAR Request Object: %w", err)
}
return requestObject, nil
}
func (c *OpenID4VPClient) RequestObjectByPost(ctx context.Context, requestURI string, walletMetadata oauth.AuthorizationServerMetadata) (string, error) {
iamClient := c.httpClient
parsedURL, err := core.ParsePublicURL(requestURI, c.strictMode)
if err != nil {
return "", fmt.Errorf("invalid request_uri: %w", err)
}

// TODO: consider adding a 'wallet_nonce'
metadataBytes, _ := json.Marshal(walletMetadata)
form := url.Values{oauth.WalletMetadataParam: {string(metadataBytes)}}
requestObject, err := iamClient.RequestObjectByPost(ctx, parsedURL.String(), form)
requestObject, err := iamClient.RequestObjectByPost(ctx, requestURI, form)
if err != nil {
return "", fmt.Errorf("failed to retrieve JAR Request Object: %w", err)
}
Expand All @@ -226,8 +212,7 @@ func (c *OpenID4VPClient) RequestObjectByPost(ctx context.Context, requestURI st

func (c *OpenID4VPClient) AccessToken(ctx context.Context, code string, tokenEndpoint string, callbackURI string, subject string, clientID string, codeVerifier string, useDPoP bool) (*oauth.TokenResponse, error) {
iamClient := c.httpClient
// validate tokenEndpoint
parsedURL, err := core.ParsePublicURL(tokenEndpoint, c.strictMode)
parsedURL, err := url.Parse(tokenEndpoint)
if err != nil {
return nil, err
}
Expand Down
5 changes: 2 additions & 3 deletions auth/client/iam/openid4vp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -822,7 +822,7 @@ func TestIAMClient_RequestObjectByGet(t *testing.T) {

response, err := ctx.client.RequestObjectByGet(context.Background(), ":")

assert.EqualError(t, err, "invalid request_uri: parse \":\": missing protocol scheme")
assert.EqualError(t, err, "failed to retrieve JAR Request Object: parse \":\": missing protocol scheme")
assert.Empty(t, response)
})
t.Run("error - failed to get access token", func(t *testing.T) {
Expand Down Expand Up @@ -853,7 +853,7 @@ func TestIAMClient_RequestObjectByPost(t *testing.T) {

response, err := ctx.client.RequestObjectByPost(context.Background(), ":", metadata)

assert.EqualError(t, err, "invalid request_uri: parse \":\": missing protocol scheme")
assert.EqualError(t, err, "failed to retrieve JAR Request Object: parse \":\": missing protocol scheme")
assert.Empty(t, response)
})
t.Run("error - failed to get access token", func(t *testing.T) {
Expand Down Expand Up @@ -884,7 +884,6 @@ func createClientTestContext(t *testing.T, tlsConfig *tls.Config) *clientTestCon
wallet: wallet,
subjectManager: subjectManager,
httpClient: HTTPClient{
strictMode: false,
httpClient: client.NewWithTLSConfig(10*time.Second, tlsConfig),
},
jwtSigner: jwtSigner,
Expand Down
16 changes: 10 additions & 6 deletions auth/oauth/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"errors"
"fmt"
"net/http"
"net/url"
"strings"

"github.com/nuts-foundation/nuts-node/core"
Expand All @@ -44,8 +45,8 @@ var ErrAllCandidates4xx = errors.New("metadata not found: every candidate return
// 2. append (OIDC Disc): https://host/<path>/.well-known/<wellKnown>
//
// When identifier has no path, insert and append collapse to a single URL. Every candidate
// shares identifier's scheme and host, so the single core.ParsePublicURL SSRF check on
// identifier covers them all.
// shares identifier's scheme and host, so the SSRF check that httpClient.Do runs on the first
// candidate it fetches covers them all.
//
// When every candidate fails, the returned error joins each candidate's failure. A per-candidate
// core.HttpError stays recoverable through the join (see errors.AsType), so callers can still
Expand All @@ -55,9 +56,9 @@ var ErrAllCandidates4xx = errors.New("metadata not found: every candidate return
func FetchMetadata[T interface {
WellKnownPath() string
GetIssuer() string
}](ctx context.Context, httpClient core.HTTPRequestDoer, identifier string, strictMode bool) (*T, error) {
}](ctx context.Context, httpClient core.HTTPRequestDoer, identifier string) (*T, error) {
var zero T
candidates, err := wellKnownCandidates(identifier, strictMode, zero.WellKnownPath())
candidates, err := wellKnownCandidates(identifier, zero.WellKnownPath())
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -113,8 +114,11 @@ func fetchMetadataCandidate[T interface{ GetIssuer() string }](ctx context.Conte
// wellKnownCandidates returns the metadata URLs to try for identifier, in priority order:
// the insert (RFC 8414) placement, then the append (OIDC Discovery) placement. When
// identifier has no path, both collapse to a single URL.
func wellKnownCandidates(identifier string, strictMode bool, wellKnown string) ([]string, error) {
identifierURL, err := core.ParsePublicURL(identifier, strictMode)
//
// identifier is not SSRF-checked here: httpClient.Do runs that check on every candidate
// before it connects, so validating it again here would just duplicate that check.
func wellKnownCandidates(identifier string, wellKnown string) ([]string, error) {
identifierURL, err := url.Parse(identifier)
if err != nil {
return nil, err
}
Expand Down
Loading
Loading