diff --git a/auth/api/auth/v1/api.go b/auth/api/auth/v1/api.go
index 1d7acb9105..6d564fcd5e 100644
--- a/auth/api/auth/v1/api.go
+++ b/auth/api/auth/v1/api.go
@@ -23,6 +23,7 @@ import (
"encoding/json"
"fmt"
"github.com/nuts-foundation/nuts-node/audit"
+ "github.com/nuts-foundation/nuts-node/auth/oauth"
"net/http"
"net/url"
"regexp"
@@ -295,7 +296,7 @@ func (w Wrapper) RequestAccessToken(ctx context.Context, request RequestAccessTo
return nil, core.InvalidInputError("invalid authorization server endpoint: %s", jwtGrant.AuthorizationServerEndpoint)
}
- accessTokenResult, err := w.Auth.RelyingParty().RequestAccessToken(ctx, jwtGrant.BearerToken, *authServerEndpoint)
+ accessTokenResult, err := w.Auth.RelyingParty().RequestRFC003AccessToken(ctx, jwtGrant.BearerToken, *authServerEndpoint)
if err != nil {
return nil, core.Error(http.StatusServiceUnavailable, err.Error())
}
@@ -310,22 +311,21 @@ func (w Wrapper) CreateAccessToken(ctx context.Context, request CreateAccessToke
if request.Body.GrantType != client.JwtBearerGrantType {
errDesc := fmt.Sprintf("grant_type must be: '%s'", client.JwtBearerGrantType)
- errorResponse := AccessTokenRequestFailedResponse{Error: errOauthUnsupportedGrant, ErrorDescription: errDesc}
+ errorResponse := oauth.ErrorResponse{Error: errOauthUnsupportedGrant, Description: &errDesc}
return CreateAccessToken400JSONResponse(errorResponse), nil
}
const jwtPattern = `^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$`
if matched, err := regexp.Match(jwtPattern, []byte(request.Body.Assertion)); !matched || err != nil {
errDesc := "Assertion must be a valid encoded jwt"
- errorResponse := AccessTokenRequestFailedResponse{Error: errOauthInvalidGrant, ErrorDescription: errDesc}
+ errorResponse := AccessTokenRequestFailedResponse{Error: errOauthInvalidGrant, Description: &errDesc}
return CreateAccessToken400JSONResponse(errorResponse), nil
}
catRequest := services.CreateAccessTokenRequest{RawJwtBearerToken: request.Body.Assertion}
acResponse, oauthError := w.Auth.AuthzServer().CreateAccessToken(ctx, catRequest)
if oauthError != nil {
- errorResponse := AccessTokenRequestFailedResponse{Error: AccessTokenRequestFailedResponseError(oauthError.Code), ErrorDescription: oauthError.Error()}
- return CreateAccessToken400JSONResponse(errorResponse), nil
+ return CreateAccessToken400JSONResponse(*oauthError), nil
}
response := AccessTokenResponse{
AccessToken: acResponse.AccessToken,
diff --git a/auth/api/auth/v1/api_test.go b/auth/api/auth/v1/api_test.go
index fd1b7a7b78..3e7e1b8604 100644
--- a/auth/api/auth/v1/api_test.go
+++ b/auth/api/auth/v1/api_test.go
@@ -28,6 +28,7 @@ import (
"github.com/nuts-foundation/nuts-node/audit"
pkg2 "github.com/nuts-foundation/nuts-node/auth"
"github.com/nuts-foundation/nuts-node/auth/contract"
+ oauth2 "github.com/nuts-foundation/nuts-node/auth/oauth"
"github.com/nuts-foundation/nuts-node/auth/services"
"github.com/nuts-foundation/nuts-node/auth/services/dummy"
"github.com/nuts-foundation/nuts-node/auth/services/oauth"
@@ -475,7 +476,7 @@ func TestWrapper_RequestAccessToken(t *testing.T) {
BearerToken: bearerToken,
AuthorizationServerEndpoint: authEndpointURL.String(),
}, nil)
- ctx.relyingPartyMock.EXPECT().RequestAccessToken(gomock.Any(), bearerToken, *authEndpointURL).Return(nil, errors.New("random error"))
+ ctx.relyingPartyMock.EXPECT().RequestRFC003AccessToken(gomock.Any(), bearerToken, *authEndpointURL).Return(nil, errors.New("random error"))
response, err := ctx.wrapper.RequestAccessToken(ctx.audit, RequestAccessTokenRequestObject{Body: &fakeRequest})
@@ -513,9 +514,10 @@ func TestWrapper_RequestAccessToken(t *testing.T) {
request := fakeRequest
request.Credentials = credentials
+ in10 := 10
expectedResponse := AccessTokenResponse{
TokenType: "token-type",
- ExpiresIn: 10,
+ ExpiresIn: &in10,
AccessToken: "actual-token",
}
@@ -532,7 +534,7 @@ func TestWrapper_RequestAccessToken(t *testing.T) {
AuthorizationServerEndpoint: authEndpointURL.String(),
}, nil)
ctx.relyingPartyMock.EXPECT().
- RequestAccessToken(gomock.Any(), bearerToken, *authEndpointURL).
+ RequestRFC003AccessToken(gomock.Any(), bearerToken, *authEndpointURL).
Return(&expectedResponse, nil)
response, err := ctx.wrapper.RequestAccessToken(ctx.audit, RequestAccessTokenRequestObject{Body: &request})
@@ -551,7 +553,7 @@ func TestWrapper_CreateAccessToken(t *testing.T) {
params := CreateAccessTokenRequest{GrantType: "unknown type"}
errorDescription := "grant_type must be: 'urn:ietf:params:oauth:grant-type:jwt-bearer'"
- expectedResponse := CreateAccessToken400JSONResponse{ErrorDescription: errorDescription, Error: errOauthUnsupportedGrant}
+ expectedResponse := CreateAccessToken400JSONResponse{Description: &errorDescription, Error: errOauthUnsupportedGrant}
response, err := ctx.wrapper.CreateAccessToken(ctx.audit, CreateAccessTokenRequestObject{Body: ¶ms})
@@ -565,7 +567,7 @@ func TestWrapper_CreateAccessToken(t *testing.T) {
params := CreateAccessTokenRequest{GrantType: "urn:ietf:params:oauth:grant-type:jwt-bearer", Assertion: "invalid jwt"}
errorDescription := "Assertion must be a valid encoded jwt"
- expectedResponse := CreateAccessToken400JSONResponse{ErrorDescription: errorDescription, Error: errOauthInvalidGrant}
+ expectedResponse := CreateAccessToken400JSONResponse{Description: &errorDescription, Error: errOauthInvalidGrant}
response, err := ctx.wrapper.CreateAccessToken(ctx.audit, CreateAccessTokenRequestObject{Body: ¶ms})
@@ -579,11 +581,11 @@ func TestWrapper_CreateAccessToken(t *testing.T) {
params := CreateAccessTokenRequest{GrantType: "urn:ietf:params:oauth:grant-type:jwt-bearer", Assertion: validJwt}
errorDescription := "oh boy"
- expectedResponse := CreateAccessToken400JSONResponse{ErrorDescription: errorDescription, Error: errOauthInvalidRequest}
+ expectedResponse := CreateAccessToken400JSONResponse{Description: &errorDescription, Error: errOauthInvalidRequest}
- ctx.authzServerMock.EXPECT().CreateAccessToken(ctx.audit, services.CreateAccessTokenRequest{RawJwtBearerToken: validJwt}).Return(nil, &oauth.ErrorResponse{
- Description: errors.New(errorDescription),
- Code: errOauthInvalidRequest,
+ ctx.authzServerMock.EXPECT().CreateAccessToken(ctx.audit, services.CreateAccessTokenRequest{RawJwtBearerToken: validJwt}).Return(nil, &oauth2.ErrorResponse{
+ Description: &errorDescription,
+ Error: errOauthInvalidRequest,
})
response, err := ctx.wrapper.CreateAccessToken(ctx.audit, CreateAccessTokenRequestObject{Body: ¶ms})
@@ -597,12 +599,13 @@ func TestWrapper_CreateAccessToken(t *testing.T) {
params := CreateAccessTokenRequest{GrantType: "urn:ietf:params:oauth:grant-type:jwt-bearer", Assertion: validJwt}
- pkgResponse := &services.AccessTokenResult{AccessToken: "foo", ExpiresIn: 800000}
+ in800000 := 800000
+ pkgResponse := &oauth2.TokenResponse{AccessToken: "foo", ExpiresIn: &in800000}
ctx.authzServerMock.EXPECT().CreateAccessToken(gomock.Any(), services.CreateAccessTokenRequest{RawJwtBearerToken: validJwt}).Return(pkgResponse, nil)
expectedResponse := CreateAccessToken200JSONResponse{
AccessToken: pkgResponse.AccessToken,
- ExpiresIn: 800000,
+ ExpiresIn: &in800000,
TokenType: "bearer",
}
diff --git a/auth/api/auth/v1/client/generated.go b/auth/api/auth/v1/client/generated.go
index 3c34151aca..c2466dc85b 100644
--- a/auth/api/auth/v1/client/generated.go
+++ b/auth/api/auth/v1/client/generated.go
@@ -20,13 +20,6 @@ const (
JwtBearerAuthScopes = "jwtBearerAuth.Scopes"
)
-// Defines values for AccessTokenRequestFailedResponseError.
-const (
- InvalidGrant AccessTokenRequestFailedResponseError = "invalid_grant"
- InvalidRequest AccessTokenRequestFailedResponseError = "invalid_request"
- UnsupportedGrantType AccessTokenRequestFailedResponseError = "unsupported_grant_type"
-)
-
// Defines values for SignSessionRequestMeans.
const (
SignSessionRequestMeansDummy SignSessionRequestMeans = "dummy"
@@ -48,17 +41,6 @@ const (
Substantial TokenIntrospectionResponseAssuranceLevel = "substantial"
)
-// AccessTokenRequestFailedResponse Error response when access token request fails as described in rfc6749 section 5.2
-type AccessTokenRequestFailedResponse struct {
- Error AccessTokenRequestFailedResponseError `json:"error"`
-
- // ErrorDescription Human-readable ASCII text providing additional information, used to assist the client developer in understanding the error that occurred.
- ErrorDescription string `json:"error_description"`
-}
-
-// AccessTokenRequestFailedResponseError defines model for AccessTokenRequestFailedResponse.Error.
-type AccessTokenRequestFailedResponseError string
-
// Contract defines model for Contract.
type Contract struct {
// Language Language of the contract in all caps.
diff --git a/auth/api/auth/v1/client/types.go b/auth/api/auth/v1/client/types.go
index 17d6e99b5e..fdccb9ca60 100644
--- a/auth/api/auth/v1/client/types.go
+++ b/auth/api/auth/v1/client/types.go
@@ -20,7 +20,7 @@ package client
import (
"github.com/nuts-foundation/go-did/vc"
- "github.com/nuts-foundation/nuts-node/auth/services"
+ "github.com/nuts-foundation/nuts-node/auth/oauth"
)
// JwtBearerGrantType defines the grant-type to use in the access token request
@@ -33,4 +33,7 @@ type VerifiableCredential = vc.VerifiableCredential
type VerifiablePresentation = vc.VerifiablePresentation
// AccessTokenResponse is an alias to use from within the API
-type AccessTokenResponse = services.AccessTokenResult
+type AccessTokenResponse = oauth.TokenResponse
+
+// AccessTokenRequestFailedResponse is an alias to use from within the API
+type AccessTokenRequestFailedResponse = oauth.ErrorResponse
diff --git a/auth/api/auth/v1/generated.go b/auth/api/auth/v1/generated.go
index 18b18e6ec6..f04ed9d8e0 100644
--- a/auth/api/auth/v1/generated.go
+++ b/auth/api/auth/v1/generated.go
@@ -18,13 +18,6 @@ const (
JwtBearerAuthScopes = "jwtBearerAuth.Scopes"
)
-// Defines values for AccessTokenRequestFailedResponseError.
-const (
- InvalidGrant AccessTokenRequestFailedResponseError = "invalid_grant"
- InvalidRequest AccessTokenRequestFailedResponseError = "invalid_request"
- UnsupportedGrantType AccessTokenRequestFailedResponseError = "unsupported_grant_type"
-)
-
// Defines values for SignSessionRequestMeans.
const (
SignSessionRequestMeansDummy SignSessionRequestMeans = "dummy"
@@ -46,17 +39,6 @@ const (
Substantial TokenIntrospectionResponseAssuranceLevel = "substantial"
)
-// AccessTokenRequestFailedResponse Error response when access token request fails as described in rfc6749 section 5.2
-type AccessTokenRequestFailedResponse struct {
- Error AccessTokenRequestFailedResponseError `json:"error"`
-
- // ErrorDescription Human-readable ASCII text providing additional information, used to assist the client developer in understanding the error that occurred.
- ErrorDescription string `json:"error_description"`
-}
-
-// AccessTokenRequestFailedResponseError defines model for AccessTokenRequestFailedResponse.Error.
-type AccessTokenRequestFailedResponseError string
-
// Contract defines model for Contract.
type Contract struct {
// Language Language of the contract in all caps.
diff --git a/auth/api/auth/v1/types.go b/auth/api/auth/v1/types.go
index 90f203be59..47da04bf1d 100644
--- a/auth/api/auth/v1/types.go
+++ b/auth/api/auth/v1/types.go
@@ -20,7 +20,7 @@ package v1
import (
"github.com/nuts-foundation/go-did/vc"
- "github.com/nuts-foundation/nuts-node/auth/services"
+ "github.com/nuts-foundation/nuts-node/auth/oauth"
)
// VerifiableCredential is an alias to use from within the API
@@ -30,4 +30,6 @@ type VerifiableCredential = vc.VerifiableCredential
type VerifiablePresentation = vc.VerifiablePresentation
// AccessTokenResponse is an alias to use from within the API
-type AccessTokenResponse = services.AccessTokenResult
+type AccessTokenResponse = oauth.TokenResponse
+
+type AccessTokenRequestFailedResponse = oauth.ErrorResponse
diff --git a/auth/api/iam/api.go b/auth/api/iam/api.go
index 9f94cf722a..f00af3399f 100644
--- a/auth/api/iam/api.go
+++ b/auth/api/iam/api.go
@@ -27,6 +27,7 @@ import (
"github.com/nuts-foundation/nuts-node/audit"
"github.com/nuts-foundation/nuts-node/auth"
"github.com/nuts-foundation/nuts-node/auth/log"
+ "github.com/nuts-foundation/nuts-node/auth/oauth"
"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/storage"
"github.com/nuts-foundation/nuts-node/vcr"
@@ -105,7 +106,7 @@ func (r Wrapper) middleware(ctx echo.Context, request interface{}, operationID s
requestCtx := context.WithValue(ctx.Request().Context(), httpRequestContextKey, ctx.Request())
ctx.SetRequest(ctx.Request().WithContext(requestCtx))
if strings.HasPrefix(ctx.Request().URL.Path, "/iam/") {
- ctx.Set(core.ErrorWriterContextKey, &oauth2ErrorWriter{})
+ ctx.Set(core.ErrorWriterContextKey, &oauth.Oauth2ErrorWriter{})
}
audit.StrictMiddleware(f, apiModuleName, operationID)
return f(ctx, request)
@@ -118,27 +119,27 @@ func (r Wrapper) HandleTokenRequest(ctx context.Context, request HandleTokenRequ
// Options:
// - OpenID4VCI
// - OpenID4VP, vp_token is sent in Token Response
- return nil, OAuth2Error{
- Code: UnsupportedGrantType,
+ return nil, oauth.OAuth2Error{
+ Code: oauth.UnsupportedGrantType,
Description: "not implemented yet",
}
case "vp_token":
// Options:
// - service-to-service vp_token flow
- return nil, OAuth2Error{
- Code: UnsupportedGrantType,
+ return nil, oauth.OAuth2Error{
+ Code: oauth.UnsupportedGrantType,
Description: "not implemented yet",
}
case "urn:ietf:params:oauth:grant-type:pre-authorized_code":
// Options:
// - OpenID4VCI
- return nil, OAuth2Error{
- Code: UnsupportedGrantType,
+ return nil, oauth.OAuth2Error{
+ Code: oauth.UnsupportedGrantType,
Description: "not implemented yet",
}
default:
- return nil, OAuth2Error{
- Code: UnsupportedGrantType,
+ return nil, oauth.OAuth2Error{
+ Code: oauth.UnsupportedGrantType,
}
}
}
@@ -160,8 +161,8 @@ func (r Wrapper) HandleAuthorizeRequest(ctx context.Context, request HandleAutho
// TODO: Spec says that the redirect URI is optional, but it's not clear what to do if it's not provided.
// Threat models say it's unsafe to omit redirect_uri.
// See https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
- return nil, OAuth2Error{
- Code: InvalidRequest,
+ return nil, oauth.OAuth2Error{
+ Code: oauth.InvalidRequest,
Description: "redirect_uri is required",
}
}
@@ -186,8 +187,8 @@ func (r Wrapper) HandleAuthorizeRequest(ctx context.Context, request HandleAutho
return r.handlePresentationRequest(params, session)
default:
// TODO: This should be a redirect?
- return nil, OAuth2Error{
- Code: UnsupportedResponseType,
+ return nil, oauth.OAuth2Error{
+ Code: oauth.UnsupportedResponseType,
RedirectURI: session.RedirectURI,
}
}
@@ -254,9 +255,9 @@ func (r Wrapper) PresentationDefinition(_ context.Context, request PresentationD
scopes := strings.Split(request.Params.Scope, " ")
presentationDefinition := r.auth.PresentationDefinitions().ByScope(scopes[0])
if presentationDefinition == nil {
- return PresentationDefinition400JSONResponse{
- Code: "invalid_scope",
- }, nil
+ return nil, oauth.OAuth2Error{
+ Code: oauth.InvalidScope,
+ }
}
return PresentationDefinition200JSONResponse(*presentationDefinition), nil
diff --git a/auth/api/iam/api_test.go b/auth/api/iam/api_test.go
index fa9119ce76..171108af15 100644
--- a/auth/api/iam/api_test.go
+++ b/auth/api/iam/api_test.go
@@ -31,6 +31,8 @@ import (
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/nuts-node/audit"
"github.com/nuts-foundation/nuts-node/auth"
+ "github.com/nuts-foundation/nuts-node/auth/oauth"
+ oauthServices "github.com/nuts-foundation/nuts-node/auth/services/oauth"
"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/storage"
"github.com/nuts-foundation/nuts-node/vcr/pe"
@@ -196,9 +198,9 @@ func TestWrapper_PresentationDefinition(t *testing.T) {
response, err := test.client.PresentationDefinition(ctx, PresentationDefinitionRequestObject{Did: webDID.ID, Params: PresentationDefinitionParams{Scope: "unknown"}})
- require.NoError(t, err)
- require.NotNil(t, response)
- assert.Equal(t, InvalidScope, (response.(PresentationDefinition400JSONResponse)).Code)
+ require.Error(t, err)
+ assert.Nil(t, response)
+ assert.Equal(t, string(oauth.InvalidScope), err.Error())
})
}
@@ -210,7 +212,7 @@ func TestWrapper_HandleAuthorizeRequest(t *testing.T) {
Id: nutsDID.String(),
})
- requireOAuthError(t, err, InvalidRequest, "redirect_uri is required")
+ requireOAuthError(t, err, oauth.InvalidRequest, "redirect_uri is required")
assert.Nil(t, res)
})
t.Run("unsupported response type", func(t *testing.T) {
@@ -223,7 +225,7 @@ func TestWrapper_HandleAuthorizeRequest(t *testing.T) {
Id: nutsDID.String(),
})
- requireOAuthError(t, err, UnsupportedResponseType, "")
+ requireOAuthError(t, err, oauth.UnsupportedResponseType, "")
assert.Nil(t, res)
})
}
@@ -239,13 +241,13 @@ func TestWrapper_HandleTokenRequest(t *testing.T) {
},
})
- requireOAuthError(t, err, UnsupportedGrantType, "")
+ requireOAuthError(t, err, oauth.UnsupportedGrantType, "")
assert.Nil(t, res)
})
}
-func requireOAuthError(t *testing.T, err error, errorCode ErrorCode, errorDescription string) {
- var oauthErr OAuth2Error
+func requireOAuthError(t *testing.T, err error, errorCode oauth.ErrorCode, errorDescription string) {
+ var oauthErr oauth.OAuth2Error
require.ErrorAs(t, err, &oauthErr)
assert.Equal(t, errorCode, oauthErr.Code)
assert.Equal(t, errorDescription, oauthErr.Description)
@@ -278,6 +280,7 @@ type testCtx struct {
authnServices *auth.MockAuthenticationServices
vdr *vdr.MockVDR
resolver *resolver.MockDIDResolver
+ relyingParty *oauthServices.MockRelyingParty
}
func newTestClient(t testing.TB) *testCtx {
@@ -288,11 +291,16 @@ func newTestClient(t testing.TB) *testCtx {
authnServices := auth.NewMockAuthenticationServices(ctrl)
authnServices.EXPECT().PublicURL().Return(publicURL).AnyTimes()
resolver := resolver.NewMockDIDResolver(ctrl)
+ relyingPary := oauthServices.NewMockRelyingParty(ctrl)
vdr := vdr.NewMockVDR(ctrl)
+
+ authnServices.EXPECT().PublicURL().Return(publicURL).AnyTimes()
+ authnServices.EXPECT().RelyingParty().Return(relyingPary).AnyTimes()
vdr.EXPECT().Resolver().Return(resolver).AnyTimes()
return &testCtx{
authnServices: authnServices,
+ relyingParty: relyingPary,
resolver: resolver,
vdr: vdr,
client: &Wrapper{
@@ -347,7 +355,7 @@ func TestWrapper_middleware(t *testing.T) {
ctx := server.NewContext(httptest.NewRequest("GET", "/iam/foo", nil), httptest.NewRecorder())
_, _ = Wrapper{auth: authService}.middleware(ctx, nil, "Test", handler.handle)
- assert.IsType(t, &oauth2ErrorWriter{}, ctx.Get(core.ErrorWriterContextKey))
+ assert.IsType(t, &oauth.Oauth2ErrorWriter{}, ctx.Get(core.ErrorWriterContextKey))
})
t.Run("other path", func(t *testing.T) {
ctx := server.NewContext(httptest.NewRequest("GET", "/internal/foo", nil), httptest.NewRecorder())
diff --git a/auth/api/iam/client.go b/auth/api/iam/client.go
deleted file mode 100644
index 2478da77fa..0000000000
--- a/auth/api/iam/client.go
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright (C) 2023 Nuts community
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- */
-
-package iam
-
-import (
- "context"
- "encoding/json"
- "fmt"
- "github.com/nuts-foundation/go-did/did"
- "github.com/nuts-foundation/nuts-node/core"
- "github.com/nuts-foundation/nuts-node/vdr/didweb"
- "io"
- "net/http"
- "net/url"
- "strings"
-)
-
-// HTTPClient holds the server address and other basic settings for the http client
-type HTTPClient struct {
- config core.ClientConfig
- httpClient core.HTTPRequestDoer
-}
-
-// NewHTTPClient creates a new api client.
-func NewHTTPClient(config core.ClientConfig) HTTPClient {
- return HTTPClient{
- config: config,
- httpClient: core.MustCreateHTTPClient(config, nil),
- }
-}
-
-// OAuthAuthorizationServerMetadata retrieves the OAuth authorization server metadata for the given web DID.
-func (hb HTTPClient) OAuthAuthorizationServerMetadata(ctx context.Context, webDID did.DID) (*OAuthAuthorizationServerMetadata, error) {
- serverURL, err := didweb.DIDToURL(webDID)
- if err != nil {
- return nil, err
- }
-
- metadataURL, err := IssuerIdToWellKnown(serverURL.String(), authzServerWellKnown, hb.config.Strictmode)
- if err != nil {
- return nil, err
- }
-
- request, err := http.NewRequest(http.MethodGet, metadataURL.String(), nil)
- if err != nil {
- return nil, err
- }
- response, err := hb.httpClient.Do(request.WithContext(ctx))
- if err != nil {
- return nil, err
- }
-
- if err = core.TestResponseCode(http.StatusOK, response); err != nil {
- return nil, err
- }
-
- var metadata OAuthAuthorizationServerMetadata
- var data []byte
-
- if data, err = io.ReadAll(response.Body); err != nil {
- return nil, fmt.Errorf("unable to read response: %w", err)
- }
- if err = json.Unmarshal(data, &metadata); err != nil {
- return nil, fmt.Errorf("unable to unmarshal response: %w, %s", err, string(data))
- }
-
- return &metadata, nil
-}
-
-// PresentationDefinition retrieves the presentation definition from the presentation definition endpoint (as specified by RFC021) for the given scope.
-func (hb HTTPClient) PresentationDefinition(ctx context.Context, definitionEndpoint string, scopes []string) (*PresentationDefinition, error) {
- presentationDefinitionURL, err := core.ParsePublicURL(definitionEndpoint, hb.config.Strictmode)
- if err != nil {
- return nil, err
- }
- presentationDefinitionURL.RawQuery = url.Values{"scope": []string{strings.Join(scopes, " ")}}.Encode()
-
- // create a GET request with scope query param
- request, err := http.NewRequest(http.MethodGet, presentationDefinitionURL.String(), nil)
- if err != nil {
- return nil, err
- }
- response, err := hb.httpClient.Do(request.WithContext(ctx))
- if err != nil {
- return nil, fmt.Errorf("failed to call endpoint: %w", err)
- }
- if httpErr := core.TestResponseCode(http.StatusOK, response); httpErr != nil {
- rse := httpErr.(core.HttpError)
- if ok, oauthErr := TestOAuthErrorCode(rse.ResponseBody, InvalidScope); ok {
- return nil, oauthErr
- }
- return nil, httpErr
- }
-
- var presentationDefinition PresentationDefinition
- var data []byte
-
- if data, err = io.ReadAll(response.Body); err != nil {
- return nil, fmt.Errorf("unable to read response: %w", err)
- }
- if err = json.Unmarshal(data, &presentationDefinition); err != nil {
- return nil, fmt.Errorf("unable to unmarshal response: %w", err)
- }
-
- return &presentationDefinition, nil
-}
diff --git a/auth/api/iam/generated.go b/auth/api/iam/generated.go
index 0af21dc2ab..9549917595 100644
--- a/auth/api/iam/generated.go
+++ b/auth/api/iam/generated.go
@@ -15,19 +15,6 @@ import (
strictecho "github.com/oapi-codegen/runtime/strictmiddleware/echo"
)
-// TokenResponse Token Responses are made as defined in (RFC6749)[https://datatracker.ietf.org/doc/html/rfc6749#section-5.1]
-type TokenResponse struct {
- // AccessToken The access token issued by the authorization server.
- AccessToken string `json:"access_token"`
-
- // ExpiresIn The lifetime in seconds of the access token.
- ExpiresIn *int `json:"expires_in,omitempty"`
- Scope *string `json:"scope,omitempty"`
-
- // TokenType The type of the token issued as described in [RFC6749].
- TokenType string `json:"token_type"`
-}
-
// PresentationDefinitionParams defines parameters for PresentationDefinition.
type PresentationDefinitionParams struct {
Scope string `form:"scope" json:"scope"`
@@ -391,21 +378,25 @@ func (response PresentationDefinition200JSONResponse) VisitPresentationDefinitio
return json.NewEncoder(w).Encode(response)
}
-type PresentationDefinition400JSONResponse ErrorResponse
+type PresentationDefinitiondefaultApplicationProblemPlusJSONResponse struct {
+ Body struct {
+ // Detail A human-readable explanation specific to this occurrence of the problem.
+ Detail string `json:"detail"`
-func (response PresentationDefinition400JSONResponse) VisitPresentationDefinitionResponse(w http.ResponseWriter) error {
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(400)
+ // Status HTTP statuscode
+ Status float32 `json:"status"`
- return json.NewEncoder(w).Encode(response)
+ // Title A short, human-readable summary of the problem type.
+ Title string `json:"title"`
+ }
+ StatusCode int
}
-type PresentationDefinition404Response struct {
-}
+func (response PresentationDefinitiondefaultApplicationProblemPlusJSONResponse) VisitPresentationDefinitionResponse(w http.ResponseWriter) error {
+ w.Header().Set("Content-Type", "application/problem+json")
+ w.WriteHeader(response.StatusCode)
-func (response PresentationDefinition404Response) VisitPresentationDefinitionResponse(w http.ResponseWriter) error {
- w.WriteHeader(404)
- return nil
+ return json.NewEncoder(w).Encode(response.Body)
}
type HandleAuthorizeRequestRequestObject struct {
@@ -531,22 +522,25 @@ func (response HandleTokenRequest200JSONResponse) VisitHandleTokenRequestRespons
return json.NewEncoder(w).Encode(response)
}
-type HandleTokenRequest400JSONResponse ErrorResponse
+type HandleTokenRequestdefaultApplicationProblemPlusJSONResponse struct {
+ Body struct {
+ // Detail A human-readable explanation specific to this occurrence of the problem.
+ Detail string `json:"detail"`
-func (response HandleTokenRequest400JSONResponse) VisitHandleTokenRequestResponse(w http.ResponseWriter) error {
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(400)
+ // Status HTTP statuscode
+ Status float32 `json:"status"`
- return json.NewEncoder(w).Encode(response)
+ // Title A short, human-readable summary of the problem type.
+ Title string `json:"title"`
+ }
+ StatusCode int
}
-type HandleTokenRequest404JSONResponse ErrorResponse
-
-func (response HandleTokenRequest404JSONResponse) VisitHandleTokenRequestResponse(w http.ResponseWriter) error {
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(404)
+func (response HandleTokenRequestdefaultApplicationProblemPlusJSONResponse) VisitHandleTokenRequestResponse(w http.ResponseWriter) error {
+ w.Header().Set("Content-Type", "application/problem+json")
+ w.WriteHeader(response.StatusCode)
- return json.NewEncoder(w).Encode(response)
+ return json.NewEncoder(w).Encode(response.Body)
}
type RequestAccessTokenRequestObject struct {
diff --git a/auth/api/iam/metadata.go b/auth/api/iam/metadata.go
index 1dae09d602..df5025ff9b 100644
--- a/auth/api/iam/metadata.go
+++ b/auth/api/iam/metadata.go
@@ -19,6 +19,7 @@
package iam
import (
+ "github.com/nuts-foundation/nuts-node/auth/oauth"
"github.com/nuts-foundation/nuts-node/core"
"net/url"
"strings"
@@ -43,8 +44,8 @@ func IssuerIdToWellKnown(issuer string, wellKnown string, strictmode bool) (*url
return issuerURL.Parse(wellKnown + issuerURL.EscapedPath())
}
-func authorizationServerMetadata(identity url.URL) OAuthAuthorizationServerMetadata {
- return OAuthAuthorizationServerMetadata{
+func authorizationServerMetadata(identity url.URL) oauth.AuthorizationServerMetadata {
+ return oauth.AuthorizationServerMetadata{
Issuer: identity.String(),
AuthorizationEndpoint: identity.JoinPath("authorize").String(),
ResponseTypesSupported: responseTypesSupported,
diff --git a/auth/api/iam/metadata_test.go b/auth/api/iam/metadata_test.go
index c0e9e5fa8e..925fa41319 100644
--- a/auth/api/iam/metadata_test.go
+++ b/auth/api/iam/metadata_test.go
@@ -19,6 +19,7 @@
package iam
import (
+ "github.com/nuts-foundation/nuts-node/auth/oauth"
"github.com/nuts-foundation/nuts-node/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -29,37 +30,39 @@ import (
func TestIssuerIdToWellKnown(t *testing.T) {
t.Run("ok", func(t *testing.T) {
issuer := "https://nuts.nl/iam/id"
- u, err := IssuerIdToWellKnown(issuer, authzServerWellKnown, true)
+ u, err := oauth.IssuerIdToWellKnown(issuer, oauth.AuthzServerWellKnown, true)
require.NoError(t, err)
assert.Equal(t, "https://nuts.nl/.well-known/oauth-authorization-server/iam/id", u.String())
})
t.Run("no path in issuer", func(t *testing.T) {
issuer := "https://nuts.nl"
- u, err := IssuerIdToWellKnown(issuer, authzServerWellKnown, true)
+ u, err := oauth.IssuerIdToWellKnown(issuer, oauth.AuthzServerWellKnown, true)
require.NoError(t, err)
assert.Equal(t, "https://nuts.nl/.well-known/oauth-authorization-server", u.String())
})
t.Run("don't unescape path", func(t *testing.T) {
issuer := "https://nuts.nl/iam/%2E%2E/still-has-iam"
- u, err := IssuerIdToWellKnown(issuer, authzServerWellKnown, true)
+ u, err := oauth.IssuerIdToWellKnown(issuer, oauth.AuthzServerWellKnown, true)
require.NoError(t, err)
assert.Equal(t, "https://nuts.nl/.well-known/oauth-authorization-server/iam/%2E%2E/still-has-iam", u.String())
})
t.Run("https in strictmode", func(t *testing.T) {
issuer := "http://nuts.nl/iam/id"
- u, err := IssuerIdToWellKnown(issuer, authzServerWellKnown, true)
+ u, err := oauth.IssuerIdToWellKnown(issuer, oauth.AuthzServerWellKnown, true)
assert.ErrorContains(t, err, "scheme must be https")
assert.Nil(t, u)
})
t.Run("no IP allowed", func(t *testing.T) {
issuer := "https://127.0.0.1/iam/id"
+
u, err := IssuerIdToWellKnown(issuer, authzServerWellKnown, true)
+
assert.ErrorContains(t, err, "hostname is IP")
assert.Nil(t, u)
})
t.Run("invalid URL", func(t *testing.T) {
issuer := "http:// /iam/id"
- u, err := IssuerIdToWellKnown(issuer, authzServerWellKnown, true)
+ u, err := oauth.IssuerIdToWellKnown(issuer, oauth.AuthzServerWellKnown, true)
assert.ErrorContains(t, err, "invalid character \" \" in host name")
assert.Nil(t, u)
})
@@ -75,7 +78,7 @@ var vpFormats = map[string]map[string][]string{
func Test_authorizationServerMetadata(t *testing.T) {
identity := "https://example.com/iam/did:nuts:123"
identityURL, _ := url.Parse(identity)
- expected := OAuthAuthorizationServerMetadata{
+ expected := oauth.AuthorizationServerMetadata{
Issuer: identity,
AuthorizationEndpoint: identity + "/authorize",
ResponseTypesSupported: []string{"code", "vp_token", "vp_token id_token"},
diff --git a/auth/api/iam/openid4vp.go b/auth/api/iam/openid4vp.go
index 5cfe9f6915..dfeb007d66 100644
--- a/auth/api/iam/openid4vp.go
+++ b/auth/api/iam/openid4vp.go
@@ -29,6 +29,7 @@ import (
ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/go-did/vc"
+ "github.com/nuts-foundation/nuts-node/auth/oauth"
"github.com/nuts-foundation/nuts-node/vcr/credential"
"github.com/nuts-foundation/nuts-node/vcr/holder"
"net/http"
@@ -85,8 +86,8 @@ func (r *Wrapper) handlePresentationRequest(params map[string]string, session *S
}
// Response mode is always direct_post for now
if params[responseModeParam] != responseModeDirectPost {
- return nil, OAuth2Error{
- Code: InvalidRequest,
+ return nil, oauth.OAuth2Error{
+ Code: oauth.InvalidRequest,
Description: "response_mode must be direct_post",
RedirectURI: session.RedirectURI,
}
@@ -96,8 +97,8 @@ func (r *Wrapper) handlePresentationRequest(params map[string]string, session *S
// For compatibility, we probably need to support presentation_definition and/or presentation_definition_uri.
presentationDefinition := r.auth.PresentationDefinitions().ByScope(params[scopeParam])
if presentationDefinition == nil {
- return nil, OAuth2Error{
- Code: InvalidRequest,
+ return nil, oauth.OAuth2Error{
+ Code: oauth.InvalidRequest,
Description: fmt.Sprintf("unsupported scope for presentation exchange: %s", params[scopeParam]),
RedirectURI: session.RedirectURI,
}
diff --git a/auth/api/iam/openid4vp_test.go b/auth/api/iam/openid4vp_test.go
index 9da7f5b192..eb55988dfc 100644
--- a/auth/api/iam/openid4vp_test.go
+++ b/auth/api/iam/openid4vp_test.go
@@ -25,6 +25,7 @@ import (
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/go-did/vc"
"github.com/nuts-foundation/nuts-node/auth"
+ "github.com/nuts-foundation/nuts-node/auth/oauth"
"github.com/nuts-foundation/nuts-node/storage"
"github.com/nuts-foundation/nuts-node/vcr"
"github.com/nuts-foundation/nuts-node/vcr/credential"
@@ -136,7 +137,7 @@ func TestWrapper_handlePresentationRequest(t *testing.T) {
response, err := instance.handlePresentationRequest(params, createSession(params, holderDID))
- requireOAuthError(t, err, InvalidRequest, "unsupported scope for presentation exchange: unsupported")
+ requireOAuthError(t, err, oauth.InvalidRequest, "unsupported scope for presentation exchange: unsupported")
assert.Nil(t, response)
})
t.Run("invalid response_mode", func(t *testing.T) {
@@ -150,7 +151,7 @@ func TestWrapper_handlePresentationRequest(t *testing.T) {
response, err := instance.handlePresentationRequest(params, createSession(params, holderDID))
- requireOAuthError(t, err, InvalidRequest, "response_mode must be direct_post")
+ requireOAuthError(t, err, oauth.InvalidRequest, "response_mode must be direct_post")
assert.Nil(t, response)
})
}
diff --git a/auth/api/iam/s2s_vptoken.go b/auth/api/iam/s2s_vptoken.go
index 7c16066277..81048adcc7 100644
--- a/auth/api/iam/s2s_vptoken.go
+++ b/auth/api/iam/s2s_vptoken.go
@@ -20,23 +20,21 @@ package iam
import (
"context"
- "crypto/rand"
- "encoding/base64"
"errors"
"fmt"
+ "github.com/nuts-foundation/nuts-node/auth/oauth"
+ "github.com/nuts-foundation/nuts-node/crypto"
+ "net/http"
+ "time"
+
"github.com/labstack/echo/v4"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/go-did/vc"
"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/storage"
"github.com/nuts-foundation/nuts-node/vdr/resolver"
- "net/http"
- "time"
)
-// secretSizeBits is the size of the generated random secrets (access tokens, pre-authorized codes) in bits.
-const secretSizeBits = 128
-
// accessTokenValidity defines how long access tokens are valid.
// TODO: Might want to make this configurable at some point
const accessTokenValidity = 15 * time.Minute
@@ -108,14 +106,17 @@ func (r Wrapper) RequestAccessToken(ctx context.Context, request RequestAccessTo
return nil, err
}
- // todo fetch metadata using didDocument service data or .well-known path
-
- return RequestAccessToken200JSONResponse{}, nil
+ tokenResult, err := r.auth.RelyingParty().RequestRFC021AccessToken(ctx, *requestHolder, *requestVerifier, request.Body.Scope)
+ if err != nil {
+ // this can be an internal server error, a 400 oauth error or a 412 precondition failed if the wallet does not contain the required credentials
+ return nil, err
+ }
+ return RequestAccessToken200JSONResponse(*tokenResult), nil
}
-func (r Wrapper) createAccessToken(issuer did.DID, issueTime time.Time, presentation vc.VerifiablePresentation, scope string) (*TokenResponse, error) {
+func (r Wrapper) createAccessToken(issuer did.DID, issueTime time.Time, presentation vc.VerifiablePresentation, scope string) (*oauth.TokenResponse, error) {
accessToken := AccessToken{
- Token: generateCode(),
+ Token: crypto.GenerateNonce(),
Issuer: issuer.String(),
Expiration: issueTime.Add(accessTokenValidity),
Presentation: presentation,
@@ -125,7 +126,7 @@ func (r Wrapper) createAccessToken(issuer did.DID, issueTime time.Time, presenta
return nil, fmt.Errorf("unable to store access token: %w", err)
}
expiresIn := int(accessTokenValidity.Seconds())
- return &TokenResponse{
+ return &oauth.TokenResponse{
AccessToken: accessToken.Token,
ExpiresIn: &expiresIn,
Scope: &scope,
@@ -137,15 +138,6 @@ func (r Wrapper) accessTokenStore(issuer did.DID) storage.SessionStore {
return r.storageEngine.GetSessionDatabase().GetStore(accessTokenValidity, "s2s", issuer.String(), "accesstoken")
}
-func generateCode() string {
- buf := make([]byte, secretSizeBits/8)
- _, err := rand.Read(buf)
- if err != nil {
- panic(err)
- }
- return base64.URLEncoding.EncodeToString(buf)
-}
-
type AccessToken struct {
Token string
Issuer string
diff --git a/auth/api/iam/s2s_vptoken_test.go b/auth/api/iam/s2s_vptoken_test.go
index 858aab47e4..c53a46c8dd 100644
--- a/auth/api/iam/s2s_vptoken_test.go
+++ b/auth/api/iam/s2s_vptoken_test.go
@@ -19,25 +19,30 @@
package iam
import (
+ "net/http"
+ "testing"
+ "time"
+
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/go-did/vc"
+ "github.com/nuts-foundation/nuts-node/auth/oauth"
+ "github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/jsonld"
"github.com/nuts-foundation/nuts-node/vdr/resolver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "testing"
- "time"
)
func TestWrapper_RequestAccessToken(t *testing.T) {
walletDID := did.MustParseDID("did:test:123")
verifierDID := did.MustParseDID("did:test:456")
- body := &RequestAccessTokenJSONRequestBody{Verifier: verifierDID.String()}
+ body := &RequestAccessTokenJSONRequestBody{Verifier: verifierDID.String(), Scope: "first second"}
t.Run("ok", func(t *testing.T) {
ctx := newTestClient(t)
ctx.vdr.EXPECT().IsOwner(nil, walletDID).Return(true, nil)
ctx.resolver.EXPECT().Resolve(verifierDID, nil).Return(&did.Document{}, &resolver.DocumentMetadata{}, nil)
+ ctx.relyingParty.EXPECT().RequestRFC021AccessToken(nil, walletDID, verifierDID, "first second").Return(&oauth.TokenResponse{}, nil)
_, err := ctx.client.RequestAccessToken(nil, RequestAccessTokenRequestObject{Did: walletDID.String(), Body: body})
@@ -59,7 +64,7 @@ func TestWrapper_RequestAccessToken(t *testing.T) {
require.EqualError(t, err, "did not found: invalid DID")
})
- t.Run("missing request body", func(t *testing.T) {
+ t.Run("error - missing request body", func(t *testing.T) {
ctx := newTestClient(t)
_, err := ctx.client.RequestAccessToken(nil, RequestAccessTokenRequestObject{Did: walletDID.String()})
@@ -67,7 +72,7 @@ func TestWrapper_RequestAccessToken(t *testing.T) {
require.Error(t, err)
assert.EqualError(t, err, "missing request body")
})
- t.Run("invalid verifier did", func(t *testing.T) {
+ t.Run("error - invalid verifier did", func(t *testing.T) {
ctx := newTestClient(t)
body := &RequestAccessTokenJSONRequestBody{Verifier: "invalid"}
ctx.vdr.EXPECT().IsOwner(nil, walletDID).Return(true, nil)
@@ -77,7 +82,7 @@ func TestWrapper_RequestAccessToken(t *testing.T) {
require.Error(t, err)
assert.EqualError(t, err, "invalid verifier: invalid DID")
})
- t.Run("verifier not found", func(t *testing.T) {
+ t.Run("error - verifier not found", func(t *testing.T) {
ctx := newTestClient(t)
ctx.vdr.EXPECT().IsOwner(nil, walletDID).Return(true, nil)
ctx.resolver.EXPECT().Resolve(verifierDID, nil).Return(nil, nil, resolver.ErrNotFound)
@@ -87,6 +92,17 @@ func TestWrapper_RequestAccessToken(t *testing.T) {
require.Error(t, err)
assert.EqualError(t, err, "verifier not found: unable to find the DID document")
})
+ t.Run("error - verifier error", func(t *testing.T) {
+ ctx := newTestClient(t)
+ ctx.vdr.EXPECT().IsOwner(nil, walletDID).Return(true, nil)
+ ctx.resolver.EXPECT().Resolve(verifierDID, nil).Return(&did.Document{}, &resolver.DocumentMetadata{}, nil)
+ ctx.relyingParty.EXPECT().RequestRFC021AccessToken(nil, walletDID, verifierDID, "first second").Return(nil, core.Error(http.StatusPreconditionFailed, "no matching credentials"))
+
+ _, err := ctx.client.RequestAccessToken(nil, RequestAccessTokenRequestObject{Did: walletDID.String(), Body: body})
+
+ require.Error(t, err)
+ assert.EqualError(t, err, "no matching credentials")
+ })
}
func TestWrapper_createAccessToken(t *testing.T) {
diff --git a/auth/api/iam/types.go b/auth/api/iam/types.go
index e633488398..d805d6a070 100644
--- a/auth/api/iam/types.go
+++ b/auth/api/iam/types.go
@@ -20,6 +20,7 @@ package iam
import (
"github.com/nuts-foundation/go-did/did"
+ "github.com/nuts-foundation/nuts-node/auth/oauth"
"github.com/nuts-foundation/nuts-node/vcr/pe"
"github.com/nuts-foundation/nuts-node/vdr/resolver"
)
@@ -30,12 +31,15 @@ type DIDDocument = did.Document
// DIDDocumentMetadata is an alias
type DIDDocumentMetadata = resolver.DocumentMetadata
-// ErrorResponse is an alias
-type ErrorResponse = OAuth2Error
-
// PresentationDefinition is an alias
type PresentationDefinition = pe.PresentationDefinition
+// TokenResponse is an alias
+type TokenResponse = oauth.TokenResponse
+
+// OAuthAuthorizationServerMetadata is an alias
+type OAuthAuthorizationServerMetadata = oauth.AuthorizationServerMetadata
+
const (
// responseTypeParam is the name of the response_type parameter.
// Specified by https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.1
@@ -159,73 +163,6 @@ const presentationSubmissionParam = "presentation_submission"
// Specified by https://openid.bitbucket.io/connect/openid-4-verifiable-presentations-1_0.html#name-response-type-vp_token
const vpTokenParam = "vp_token"
-// OAuthAuthorizationServerMetadata defines the OAuth Authorization Server metadata.
-// Specified by https://www.rfc-editor.org/rfc/rfc8414.txt
-type OAuthAuthorizationServerMetadata struct {
- // Issuer defines the authorization server's identifier, which is a URL that uses the "https" scheme and has no query or fragment components.
- Issuer string `json:"issuer"`
-
- /* ******** /authorize ******** */
-
- // AuthorizationEndpoint defines the URL of the authorization server's authorization endpoint [RFC6749]
- AuthorizationEndpoint string `json:"authorization_endpoint"`
-
- // ResponseTypesSupported defines what response types a client can request
- ResponseTypesSupported []string `json:"response_types_supported,omitempty"`
-
- // ResponseModesSupported defines what response modes a client can request
- // Currently supports
- // - query for response_type=code
- // - direct_post for response_type=["vp_token", "vp_token id_token"]
- // TODO: is `form_post` something we want in the future?
- ResponseModesSupported []string `json:"response_modes_supported,omitempty"`
-
- /* ******** /token ******** */
-
- // TokenEndpoint defines the URL of the authorization server's token endpoint [RFC6749].
- TokenEndpoint string `json:"token_endpoint"`
-
- // GrantTypesSupported is a list of the OAuth 2.0 grant type values that this authorization server supports.
- GrantTypesSupported []string `json:"grant_types_supported,omitempty"`
-
- //// TODO: what do we support?
- //// TokenEndpointAuthMethodsSupported is a JSON array containing a list of client authentication methods supported by this token endpoint.
- //// Client authentication method values are used in the "token_endpoint_auth_method" parameter defined in Section 2 of [RFC7591].
- //// If omitted, the default is "client_secret_basic" -- the HTTP Basic Authentication Scheme specified in Section 2.3.1 of OAuth 2.0 [RFC6749].
- //TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported,omitempty"`
- //
- //// TODO: May be needed depending on TokenEndpointAuthMethodsSupported
- //// TokenEndpointAuthSigningAlgValuesSupported is a JSON array containing a list of the JWS signing algorithms ("alg" values) supported by the token endpoint
- //// for the signature on the JWT [JWT] used to authenticate the client at the token endpoint for the "private_key_jwt" and "client_secret_jwt" authentication methods.
- //// This metadata entry MUST be present if either of these authentication methods are specified in the "token_endpoint_auth_methods_supported" entry.
- //// No default algorithms are implied if this entry is omitted. Servers SHOULD support "RS256". The value "none" MUST NOT be used.
- //TokenEndpointAuthSigningAlgValuesSupported []string `json:"token_endpoint_auth_signing_alg_values_supported,omitempty"`
-
- /* ******** openid4vc ******** */
-
- // PreAuthorizedGrantAnonymousAccessSupported indicates whether anonymous access (requests without client_id) for pre-authorized code grant flows.
- // See https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-oauth-20-authorization-serv
- PreAuthorizedGrantAnonymousAccessSupported bool `json:"pre-authorized_grant_anonymous_access_supported,omitempty"`
-
- // PresentationDefinitionEndpoint defines the URL of the authorization server's presentation definition endpoint.
- // See https://nuts-foundation.gitbook.io/drafts/rfc/rfc021-vp_token-grant-type
- PresentationDefinitionEndpoint string `json:"presentation_definition_endpoint,omitempty"`
-
- // PresentationDefinitionUriSupported specifies whether the Wallet supports the transfer of presentation_definition by reference, with true indicating support.
- // If omitted, the default value is true. (hence pointer, or add custom unmarshalling)
- PresentationDefinitionUriSupported *bool `json:"presentation_definition_uri_supported,omitempty"`
-
- // VPFormatsSupported is an object containing a list of key value pairs, where the key is a string identifying a Credential format supported by the Wallet.
- VPFormatsSupported map[string]map[string][]string `json:"vp_formats_supported,omitempty"`
-
- // VPFormats is an object containing a list of key value pairs, where the key is a string identifying a Credential format supported by the Verifier.
- VPFormats map[string]map[string][]string `json:"vp_formats,omitempty"`
-
- // ClientIdSchemesSupported defines the `client_id_schemes` currently supported.
- // If omitted, the default value is `pre-registered` (referring to the client), which is currently not supported.
- ClientIdSchemesSupported []string `json:"client_id_schemes_supported,omitempty"`
-}
-
// OAuthClientMetadata defines the OAuth Client metadata.
// Specified by https://www.rfc-editor.org/rfc/rfc7591.html and elsewhere.
type OAuthClientMetadata struct {
diff --git a/auth/auth.go b/auth/auth.go
index 0db58e7484..f77a801d31 100644
--- a/auth/auth.go
+++ b/auth/auth.go
@@ -168,7 +168,7 @@ func (auth *Auth) Configure(config core.ServerConfig) error {
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, time.Duration(auth.config.HTTPTimeout)*time.Second, tlsConfig)
+ auth.keyStore, auth.vcr.Wallet(), time.Duration(auth.config.HTTPTimeout)*time.Second, tlsConfig, config.Strictmode)
if err := auth.authzServer.Configure(auth.config.ClockSkew, config.Strictmode); err != nil {
return err
diff --git a/auth/client/iam/client.go b/auth/client/iam/client.go
new file mode 100644
index 0000000000..0bb167fc3c
--- /dev/null
+++ b/auth/client/iam/client.go
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2023 Nuts community
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package iam
+
+import (
+ "context"
+ "crypto/tls"
+ "encoding/json"
+ "fmt"
+ "github.com/nuts-foundation/nuts-node/auth/log"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+
+ "github.com/nuts-foundation/go-did/did"
+ "github.com/nuts-foundation/go-did/vc"
+ "github.com/nuts-foundation/nuts-node/auth/oauth"
+ "github.com/nuts-foundation/nuts-node/core"
+ "github.com/nuts-foundation/nuts-node/vcr/pe"
+ "github.com/nuts-foundation/nuts-node/vdr/didweb"
+)
+
+// HTTPClient holds the server address and other basic settings for the http client
+type HTTPClient struct {
+ strictMode bool
+ httpClient core.HTTPRequestDoer
+}
+
+// NewHTTPClient creates a new api client.
+func NewHTTPClient(strictMode bool, timeout time.Duration, tlsConfig *tls.Config) HTTPClient {
+ return HTTPClient{
+ strictMode: strictMode,
+ httpClient: core.NewStrictHTTPClient(strictMode, timeout, tlsConfig),
+ }
+}
+
+// OAuthAuthorizationServerMetadata retrieves the OAuth authorization server metadata for the given web DID.
+func (hb HTTPClient) OAuthAuthorizationServerMetadata(ctx context.Context, webDID did.DID) (*oauth.AuthorizationServerMetadata, error) {
+ serverURL, err := didweb.DIDToURL(webDID)
+ if err != nil {
+ return nil, err
+ }
+
+ metadataURL, err := oauth.IssuerIdToWellKnown(serverURL.String(), oauth.AuthzServerWellKnown, hb.strictMode)
+ if err != nil {
+ return nil, err
+ }
+
+ request, err := http.NewRequestWithContext(ctx, http.MethodGet, metadataURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+ response, err := hb.httpClient.Do(request.WithContext(ctx))
+ if err != nil {
+ return nil, err
+ }
+
+ if err = core.TestResponseCode(http.StatusOK, response); err != nil {
+ return nil, err
+ }
+
+ var metadata oauth.AuthorizationServerMetadata
+ var data []byte
+
+ if data, err = io.ReadAll(response.Body); err != nil {
+ return nil, fmt.Errorf("unable to read response: %w", err)
+ }
+ if err = json.Unmarshal(data, &metadata); err != nil {
+ return nil, fmt.Errorf("unable to unmarshal response: %w, %s", err, string(data))
+ }
+
+ return &metadata, nil
+}
+
+// PresentationDefinition retrieves the presentation definition from the presentation definition endpoint (as specified by RFC021) for the given scope.
+func (hb HTTPClient) PresentationDefinition(ctx context.Context, definitionEndpoint string, scopes string) (*pe.PresentationDefinition, error) {
+ presentationDefinitionURL, err := core.ParsePublicURL(definitionEndpoint, hb.strictMode)
+
+ if err != nil {
+ return nil, err
+ }
+ presentationDefinitionURL.RawQuery = url.Values{"scope": []string{scopes}}.Encode()
+
+ // create a GET request with scope query param
+ request, err := http.NewRequestWithContext(ctx, http.MethodGet, presentationDefinitionURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+ response, err := hb.httpClient.Do(request.WithContext(ctx))
+ if err != nil {
+ return nil, fmt.Errorf("failed to call endpoint: %w", err)
+ }
+ if httpErr := core.TestResponseCode(http.StatusOK, response); httpErr != nil {
+ rse := httpErr.(core.HttpError)
+ if ok, oauthErr := oauth.TestOAuthErrorCode(rse.ResponseBody, oauth.InvalidScope); ok {
+ return nil, oauthErr
+ }
+ return nil, httpErr
+ }
+
+ var presentationDefinition pe.PresentationDefinition
+ var data []byte
+
+ if data, err = io.ReadAll(response.Body); err != nil {
+ return nil, fmt.Errorf("unable to read response: %w", err)
+ }
+ if err = json.Unmarshal(data, &presentationDefinition); err != nil {
+ return nil, fmt.Errorf("unable to unmarshal response: %w", err)
+ }
+
+ return &presentationDefinition, nil
+}
+
+func (hb HTTPClient) AccessToken(ctx context.Context, tokenEndpoint string, vp vc.VerifiablePresentation, submission pe.PresentationSubmission, scopes string) (oauth.TokenResponse, error) {
+ var token oauth.TokenResponse
+ presentationDefinitionURL, err := url.Parse(tokenEndpoint)
+ if err != nil {
+ return token, err
+ }
+
+ // create a POST request with x-www-form-urlencoded body
+ assertion, _ := json.Marshal(vp)
+ presentationSubmission, _ := json.Marshal(submission)
+ data := url.Values{}
+ data.Set(oauth.GrantTypeParam, oauth.VpTokenGrantType)
+ data.Set(oauth.AssertionParam, string(assertion))
+ data.Set(oauth.PresentationSubmissionParam, string(presentationSubmission))
+ data.Set(oauth.ScopeParam, scopes)
+ request, err := http.NewRequestWithContext(ctx, http.MethodPost, presentationDefinitionURL.String(), strings.NewReader(data.Encode()))
+ request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ if err != nil {
+ return token, err
+ }
+ response, err := hb.httpClient.Do(request.WithContext(ctx))
+ if err != nil {
+ return token, fmt.Errorf("failed to call endpoint: %w", err)
+ }
+ if err = core.TestResponseCode(http.StatusOK, response); err != nil {
+ // check for oauth error
+ if innerErr := core.TestResponseCode(http.StatusBadRequest, response); innerErr != nil {
+ // a non oauth error, the response body could contain a lot of stuff. We'll log and return the entire error
+ log.Logger().Debugf("authorization server token endpoint returned non oauth error (statusCode=%d)", response.StatusCode)
+ }
+
+ return token, err
+ }
+
+ var responseData []byte
+ if responseData, err = io.ReadAll(response.Body); err != nil {
+ return token, fmt.Errorf("unable to read response: %w", err)
+ }
+ if err = json.Unmarshal(responseData, &token); err != nil {
+ // Cut off the response body to 100 characters max to prevent logging of large responses
+ responseBodyString := string(responseData)
+ if len(responseBodyString) > 100 {
+ responseBodyString = responseBodyString[:100] + "...(clipped)"
+ }
+ return token, fmt.Errorf("unable to unmarshal response: %w, %s", err, string(responseData))
+ }
+ return token, nil
+}
diff --git a/auth/api/iam/client_test.go b/auth/client/iam/client_test.go
similarity index 67%
rename from auth/api/iam/client_test.go
rename to auth/client/iam/client_test.go
index cff6e5d166..5df91fbfcd 100644
--- a/auth/api/iam/client_test.go
+++ b/auth/client/iam/client_test.go
@@ -21,25 +21,29 @@ package iam
import (
"context"
"github.com/nuts-foundation/go-did/did"
+ "github.com/nuts-foundation/go-did/vc"
+ "github.com/nuts-foundation/nuts-node/auth/oauth"
+ "github.com/nuts-foundation/nuts-node/core"
http2 "github.com/nuts-foundation/nuts-node/test/http"
+ "github.com/nuts-foundation/nuts-node/vcr/pe"
"github.com/nuts-foundation/nuts-node/vdr/didweb"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"net/http"
"net/http/httptest"
"net/url"
- "strings"
"testing"
+ "time"
)
func TestHTTPClient_OAuthAuthorizationServerMetadata(t *testing.T) {
ctx := context.Background()
t.Run("ok using root web:did", func(t *testing.T) {
- result := OAuthAuthorizationServerMetadata{TokenEndpoint: "/token"}
+ result := oauth.AuthorizationServerMetadata{TokenEndpoint: "/token"}
handler := http2.Handler{StatusCode: http.StatusOK, ResponseData: result}
tlsServer, client := testServerAndClient(t, &handler)
- testDID := stringURLToDID(t, tlsServer.URL)
+ testDID := didweb.ServerURLToDIDWeb(t, tlsServer.URL)
metadata, err := client.OAuthAuthorizationServerMetadata(ctx, testDID)
@@ -51,10 +55,10 @@ func TestHTTPClient_OAuthAuthorizationServerMetadata(t *testing.T) {
assert.Equal(t, "/.well-known/oauth-authorization-server", handler.Request.URL.Path)
})
t.Run("ok using user web:did", func(t *testing.T) {
- result := OAuthAuthorizationServerMetadata{TokenEndpoint: "/token"}
+ result := oauth.AuthorizationServerMetadata{TokenEndpoint: "/token"}
handler := http2.Handler{StatusCode: http.StatusOK, ResponseData: result}
tlsServer, client := testServerAndClient(t, &handler)
- testDID := stringURLToDID(t, tlsServer.URL)
+ testDID := didweb.ServerURLToDIDWeb(t, tlsServer.URL)
testDID = did.MustParseDID(testDID.String() + ":iam:123")
metadata, err := client.OAuthAuthorizationServerMetadata(ctx, testDID)
@@ -69,7 +73,7 @@ func TestHTTPClient_OAuthAuthorizationServerMetadata(t *testing.T) {
t.Run("error - non 200 return value", func(t *testing.T) {
handler := http2.Handler{StatusCode: http.StatusBadRequest}
tlsServer, client := testServerAndClient(t, &handler)
- testDID := stringURLToDID(t, tlsServer.URL)
+ testDID := didweb.ServerURLToDIDWeb(t, tlsServer.URL)
metadata, err := client.OAuthAuthorizationServerMetadata(ctx, testDID)
@@ -79,7 +83,7 @@ func TestHTTPClient_OAuthAuthorizationServerMetadata(t *testing.T) {
t.Run("error - bad contents", func(t *testing.T) {
handler := http2.Handler{StatusCode: http.StatusOK, ResponseData: "not json"}
tlsServer, client := testServerAndClient(t, &handler)
- testDID := stringURLToDID(t, tlsServer.URL)
+ testDID := didweb.ServerURLToDIDWeb(t, tlsServer.URL)
metadata, err := client.OAuthAuthorizationServerMetadata(ctx, testDID)
@@ -88,7 +92,7 @@ func TestHTTPClient_OAuthAuthorizationServerMetadata(t *testing.T) {
})
t.Run("error - server not responding", func(t *testing.T) {
_, client := testServerAndClient(t, nil)
- testDID := stringURLToDID(t, "https://localhost:1234")
+ testDID := didweb.ServerURLToDIDWeb(t, "https://localhost:1234")
metadata, err := client.OAuthAuthorizationServerMetadata(ctx, testDID)
@@ -99,7 +103,7 @@ func TestHTTPClient_OAuthAuthorizationServerMetadata(t *testing.T) {
func TestHTTPClient_PresentationDefinition(t *testing.T) {
ctx := context.Background()
- definition := PresentationDefinition{
+ definition := pe.PresentationDefinition{
Id: "123",
}
@@ -107,7 +111,7 @@ func TestHTTPClient_PresentationDefinition(t *testing.T) {
handler := http2.Handler{StatusCode: http.StatusOK, ResponseData: definition}
tlsServer, client := testServerAndClient(t, &handler)
- response, err := client.PresentationDefinition(ctx, tlsServer.URL, []string{"test"})
+ response, err := client.PresentationDefinition(ctx, tlsServer.URL, "test")
require.NoError(t, err)
require.NotNil(t, definition)
@@ -118,7 +122,7 @@ func TestHTTPClient_PresentationDefinition(t *testing.T) {
handler := http2.Handler{StatusCode: http.StatusOK, ResponseData: definition}
tlsServer, client := testServerAndClient(t, &handler)
- response, err := client.PresentationDefinition(ctx, tlsServer.URL, []string{"first", "second"})
+ response, err := client.PresentationDefinition(ctx, tlsServer.URL, "first second")
require.NoError(t, err)
require.NotNil(t, definition)
@@ -127,10 +131,10 @@ func TestHTTPClient_PresentationDefinition(t *testing.T) {
assert.Equal(t, url.Values{"scope": []string{"first second"}}, handler.Request.URL.Query())
})
t.Run("error - invalid_scope", func(t *testing.T) {
- handler := http2.Handler{StatusCode: http.StatusBadRequest, ResponseData: OAuth2Error{Code: InvalidScope}}
+ handler := http2.Handler{StatusCode: http.StatusBadRequest, ResponseData: oauth.OAuth2Error{Code: oauth.InvalidScope}}
tlsServer, client := testServerAndClient(t, &handler)
- response, err := client.PresentationDefinition(ctx, tlsServer.URL, []string{"test"})
+ response, err := client.PresentationDefinition(ctx, tlsServer.URL, "test")
require.Error(t, err)
assert.EqualError(t, err, "invalid_scope")
@@ -140,7 +144,7 @@ func TestHTTPClient_PresentationDefinition(t *testing.T) {
handler := http2.Handler{StatusCode: http.StatusNotFound}
tlsServer, client := testServerAndClient(t, &handler)
- response, err := client.PresentationDefinition(ctx, tlsServer.URL, []string{"test"})
+ response, err := client.PresentationDefinition(ctx, tlsServer.URL, "test")
require.Error(t, err)
assert.EqualError(t, err, "server returned HTTP 404 (expected: 200)")
@@ -150,7 +154,7 @@ func TestHTTPClient_PresentationDefinition(t *testing.T) {
handler := http2.Handler{StatusCode: http.StatusNotFound}
_, client := testServerAndClient(t, &handler)
- response, err := client.PresentationDefinition(ctx, ":", []string{"test"})
+ response, err := client.PresentationDefinition(ctx, ":", "test")
require.Error(t, err)
assert.EqualError(t, err, "parse \":\": missing protocol scheme")
@@ -160,7 +164,7 @@ func TestHTTPClient_PresentationDefinition(t *testing.T) {
handler := http2.Handler{StatusCode: http.StatusNotFound}
_, client := testServerAndClient(t, &handler)
- response, err := client.PresentationDefinition(ctx, "http://localhost", []string{"test"})
+ response, err := client.PresentationDefinition(ctx, "http://localhost", "test")
require.Error(t, err)
assert.ErrorContains(t, err, "connection refused")
@@ -170,7 +174,7 @@ func TestHTTPClient_PresentationDefinition(t *testing.T) {
handler := http2.Handler{StatusCode: http.StatusOK, ResponseData: "}"}
tlsServer, client := testServerAndClient(t, &handler)
- response, err := client.PresentationDefinition(ctx, tlsServer.URL, []string{"test"})
+ response, err := client.PresentationDefinition(ctx, tlsServer.URL, "test")
require.Error(t, err)
assert.EqualError(t, err, "unable to unmarshal response: invalid character '}' looking for beginning of value")
@@ -178,18 +182,66 @@ func TestHTTPClient_PresentationDefinition(t *testing.T) {
})
}
+func TestHTTPClient_AccessToken(t *testing.T) {
+ t.Run("ok", func(t *testing.T) {
+ ctx := context.Background()
+ now := int(time.Now().Unix())
+ scope := "test"
+ accessToken := oauth.TokenResponse{
+ AccessToken: "token",
+ TokenType: "bearer",
+ Scope: &scope,
+ ExpiresIn: &now,
+ }
+ vp := vc.VerifiablePresentation{}
+
+ t.Run("ok", func(t *testing.T) {
+ handler := http2.Handler{StatusCode: http.StatusOK, ResponseData: accessToken}
+ tlsServer, client := testServerAndClient(t, &handler)
+
+ response, err := client.AccessToken(ctx, tlsServer.URL, vp, pe.PresentationSubmission{}, "test")
+
+ require.NoError(t, err)
+ require.NotNil(t, response)
+ assert.Equal(t, "token", response.AccessToken)
+ assert.Equal(t, "bearer", response.TokenType)
+ require.NotNil(t, response.Scope)
+ assert.Equal(t, "test", *response.Scope)
+ require.NotNil(t, response.ExpiresIn)
+ assert.Equal(t, now, *response.ExpiresIn)
+ })
+ })
+ t.Run("error - oauth error", func(t *testing.T) {
+ ctx := context.Background()
+ handler := http2.Handler{StatusCode: http.StatusBadRequest, ResponseData: oauth.OAuth2Error{Code: oauth.InvalidScope}}
+ tlsServer, client := testServerAndClient(t, &handler)
+
+ _, err := client.AccessToken(ctx, tlsServer.URL, vc.VerifiablePresentation{}, pe.PresentationSubmission{}, "test")
+
+ require.Error(t, err)
+ // check if the error is a http error
+ httpError, ok := err.(core.HttpError)
+ require.True(t, ok)
+ assert.Equal(t, "{\"error\":\"invalid_scope\"}", string(httpError.ResponseBody))
+ })
+ t.Run("error - generic server error", func(t *testing.T) {
+ ctx := context.Background()
+ handler := http2.Handler{StatusCode: http.StatusBadGateway, ResponseData: "offline"}
+ tlsServer, client := testServerAndClient(t, &handler)
+
+ _, err := client.AccessToken(ctx, tlsServer.URL, vc.VerifiablePresentation{}, pe.PresentationSubmission{}, "test")
+
+ require.Error(t, err)
+ // check if the error is a http error
+ httpError, ok := err.(core.HttpError)
+ require.True(t, ok)
+ assert.Equal(t, "offline", string(httpError.ResponseBody))
+ })
+}
+
func testServerAndClient(t *testing.T, handler http.Handler) (*httptest.Server, *HTTPClient) {
tlsServer := http2.TestTLSServer(t, handler)
return tlsServer, &HTTPClient{
httpClient: tlsServer.Client(),
}
}
-
-func stringURLToDID(t *testing.T, stringUrl string) did.DID {
- stringUrl = strings.ReplaceAll(stringUrl, "127.0.0.1", "localhost")
- asURL, err := url.Parse(stringUrl)
- require.NoError(t, err)
- testDID, err := didweb.URLToDID(*asURL)
- require.NoError(t, err)
- return *testDID
-}
diff --git a/auth/interface.go b/auth/interface.go
index 2ad318a603..f508d3e958 100644
--- a/auth/interface.go
+++ b/auth/interface.go
@@ -25,6 +25,9 @@ import (
"net/url"
)
+// ModuleName contains the name of this module
+const ModuleName = "Auth"
+
// AuthenticationServices is the interface which should be implemented for clients or mocks
type AuthenticationServices interface {
// AuthzServer returns the oauth.AuthorizationServer
diff --git a/auth/api/iam/error.go b/auth/oauth/error.go
similarity index 97%
rename from auth/api/iam/error.go
rename to auth/oauth/error.go
index 41001fd554..23e98d8f48 100644
--- a/auth/api/iam/error.go
+++ b/auth/oauth/error.go
@@ -16,7 +16,7 @@
*
*/
-package iam
+package oauth
import (
"encoding/json"
@@ -88,9 +88,10 @@ func (e OAuth2Error) Error() string {
return strings.Join(parts, " - ")
}
-type oauth2ErrorWriter struct{}
+// Oauth2ErrorWriter is a HTTP response writer for OAuth errors
+type Oauth2ErrorWriter struct{}
-func (p oauth2ErrorWriter) Write(echoContext echo.Context, _ int, _ string, err error) error {
+func (p Oauth2ErrorWriter) Write(echoContext echo.Context, _ int, _ string, err error) error {
var oauthErr OAuth2Error
if !errors.As(err, &oauthErr) {
// Internal error, wrap it in an OAuth2 error
diff --git a/auth/api/iam/error_test.go b/auth/oauth/error_test.go
similarity index 92%
rename from auth/api/iam/error_test.go
rename to auth/oauth/error_test.go
index 9225282c92..0e95bf536e 100644
--- a/auth/api/iam/error_test.go
+++ b/auth/oauth/error_test.go
@@ -16,7 +16,7 @@
*
*/
-package iam
+package oauth
import (
"errors"
@@ -45,7 +45,7 @@ func Test_oauth2ErrorWriter_Write(t *testing.T) {
rec := httptest.NewRecorder()
ctx := server.NewContext(httpRequest, rec)
- err := oauth2ErrorWriter{}.Write(ctx, 0, "", OAuth2Error{
+ err := Oauth2ErrorWriter{}.Write(ctx, 0, "", OAuth2Error{
Code: InvalidRequest,
Description: "failure",
RedirectURI: "https://example.com",
@@ -61,7 +61,7 @@ func Test_oauth2ErrorWriter_Write(t *testing.T) {
rec := httptest.NewRecorder()
ctx := server.NewContext(httpRequest, rec)
- err := oauth2ErrorWriter{}.Write(ctx, 0, "", OAuth2Error{
+ err := Oauth2ErrorWriter{}.Write(ctx, 0, "", OAuth2Error{
Code: InvalidRequest,
Description: "failure",
})
@@ -80,7 +80,7 @@ func Test_oauth2ErrorWriter_Write(t *testing.T) {
rec := httptest.NewRecorder()
ctx := server.NewContext(httpRequest, rec)
- err := oauth2ErrorWriter{}.Write(ctx, 0, "", OAuth2Error{
+ err := Oauth2ErrorWriter{}.Write(ctx, 0, "", OAuth2Error{
Code: InvalidRequest,
Description: "failure",
})
@@ -98,7 +98,7 @@ func Test_oauth2ErrorWriter_Write(t *testing.T) {
rec := httptest.NewRecorder()
ctx := server.NewContext(httpRequest, rec)
- err := oauth2ErrorWriter{}.Write(ctx, 0, "", OAuth2Error{
+ err := Oauth2ErrorWriter{}.Write(ctx, 0, "", OAuth2Error{
Description: "failure",
})
@@ -113,7 +113,7 @@ func Test_oauth2ErrorWriter_Write(t *testing.T) {
rec := httptest.NewRecorder()
ctx := server.NewContext(httpRequest, rec)
- err := oauth2ErrorWriter{}.Write(ctx, 0, "", errors.New("catastrophic"))
+ err := Oauth2ErrorWriter{}.Write(ctx, 0, "", errors.New("catastrophic"))
assert.NoError(t, err)
body, _ := io.ReadAll(rec.Body)
diff --git a/auth/oauth/types.go b/auth/oauth/types.go
new file mode 100644
index 0000000000..abc14df714
--- /dev/null
+++ b/auth/oauth/types.go
@@ -0,0 +1,138 @@
+/*
+ * Nuts node
+ * Copyright (C) 2023 Nuts community
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+// Package oauth contains generic OAuth related functionality, variables and constants
+package oauth
+
+import (
+ "github.com/nuts-foundation/nuts-node/core"
+ "net/url"
+)
+
+// this file contains constants, variables and helper functions for OAuth related code
+
+// TokenResponse is the OAuth access token response
+type TokenResponse struct {
+ AccessToken string `json:"access_token"`
+ ExpiresIn *int `json:"expires_in,omitempty"`
+ TokenType string `json:"token_type"`
+ CNonce *string `json:"c_nonce,omitempty"`
+ Scope *string `json:"scope,omitempty"`
+}
+
+const (
+ // AuthzServerWellKnown is the well-known base path for the oauth authorization server metadata as defined in RFC8414
+ AuthzServerWellKnown = "/.well-known/oauth-authorization-server"
+ // openidCredIssuerWellKnown is the well-known base path for the openID credential issuer metadata as defined in OpenID4VCI specification
+ openidCredIssuerWellKnown = "/.well-known/openid-credential-issuer"
+ // openidCredWalletWellKnown is the well-known path element we created for openid4vci to retrieve the oauth client metadata
+ openidCredWalletWellKnown = "/.well-known/openid-credential-wallet"
+ // GrantTypeParam is the parameter name for the grant_type parameter
+ GrantTypeParam = "grant_type"
+ // AssertionParam is the parameter name for the assertion parameter
+ AssertionParam = "assertion"
+ // ScopeParam is the parameter name for the scope parameter
+ ScopeParam = "scope"
+ // PresentationSubmissionParam is the parameter name for the presentation_submission parameter
+ PresentationSubmissionParam = "presentation_submission"
+ // VpTokenGrantType is the grant_type for the vp_token-bearer grant type
+ VpTokenGrantType = "vp_token-bearer"
+)
+
+// IssuerIdToWellKnown converts the OAuth2 Issuer identity to the specified well-known endpoint by inserting the well-known at the root of the path.
+// It returns no url and an error when issuer is not a valid URL.
+func IssuerIdToWellKnown(issuer string, wellKnown string, strictmode bool) (*url.URL, error) {
+ issuerURL, err := core.ParsePublicURL(issuer, strictmode)
+ if err != nil {
+ return nil, err
+ }
+ return issuerURL.Parse(wellKnown + issuerURL.EscapedPath())
+}
+
+// AuthorizationServerMetadata defines the OAuth Authorization Server metadata.
+// Specified by https://www.rfc-editor.org/rfc/rfc8414.txt
+type AuthorizationServerMetadata struct {
+ // Issuer defines the authorization server's identifier, which is a URL that uses the "https" scheme and has no query or fragment components.
+ Issuer string `json:"issuer"`
+
+ /* ******** /authorize ******** */
+
+ // AuthorizationEndpoint defines the URL of the authorization server's authorization endpoint [RFC6749]
+ AuthorizationEndpoint string `json:"authorization_endpoint"`
+
+ // ResponseTypesSupported defines what response types a client can request
+ ResponseTypesSupported []string `json:"response_types_supported,omitempty"`
+
+ // ResponseModesSupported defines what response modes a client can request
+ // Currently supports
+ // - query for response_type=code
+ // - direct_post for response_type=["vp_token", "vp_token id_token"]
+ // TODO: is `form_post` something we want in the future?
+ ResponseModesSupported []string `json:"response_modes_supported,omitempty"`
+
+ /* ******** /token ******** */
+
+ // TokenEndpoint defines the URL of the authorization server's token endpoint [RFC6749].
+ TokenEndpoint string `json:"token_endpoint"`
+
+ // GrantTypesSupported is a list of the OAuth 2.0 grant type values that this authorization server supports.
+ GrantTypesSupported []string `json:"grant_types_supported,omitempty"`
+
+ //// TODO: what do we support?
+ //// TokenEndpointAuthMethodsSupported is a JSON array containing a list of client authentication methods supported by this token endpoint.
+ //// Client authentication method values are used in the "token_endpoint_auth_method" parameter defined in Section 2 of [RFC7591].
+ //// If omitted, the default is "client_secret_basic" -- the HTTP Basic Authentication Scheme specified in Section 2.3.1 of OAuth 2.0 [RFC6749].
+ //TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported,omitempty"`
+ //
+ //// TODO: May be needed depending on TokenEndpointAuthMethodsSupported
+ //// TokenEndpointAuthSigningAlgValuesSupported is a JSON array containing a list of the JWS signing algorithms ("alg" values) supported by the token endpoint
+ //// for the signature on the JWT [JWT] used to authenticate the client at the token endpoint for the "private_key_jwt" and "client_secret_jwt" authentication methods.
+ //// This metadata entry MUST be present if either of these authentication methods are specified in the "token_endpoint_auth_methods_supported" entry.
+ //// No default algorithms are implied if this entry is omitted. Servers SHOULD support "RS256". The value "none" MUST NOT be used.
+ //TokenEndpointAuthSigningAlgValuesSupported []string `json:"token_endpoint_auth_signing_alg_values_supported,omitempty"`
+
+ /* ******** openid4vc ******** */
+
+ // PreAuthorizedGrantAnonymousAccessSupported indicates whether anonymous access (requests without client_id) for pre-authorized code grant flows.
+ // See https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-oauth-20-authorization-serv
+ PreAuthorizedGrantAnonymousAccessSupported bool `json:"pre-authorized_grant_anonymous_access_supported,omitempty"`
+
+ // PresentationDefinitionEndpoint defines the URL of the authorization server's presentation definition endpoint.
+ // See https://nuts-foundation.gitbook.io/drafts/rfc/rfc021-vp_token-grant-type
+ PresentationDefinitionEndpoint string `json:"presentation_definition_endpoint,omitempty"`
+
+ // PresentationDefinitionUriSupported specifies whether the Wallet supports the transfer of presentation_definition by reference, with true indicating support.
+ // If omitted, the default value is true. (hence pointer, or add custom unmarshalling)
+ PresentationDefinitionUriSupported *bool `json:"presentation_definition_uri_supported,omitempty"`
+
+ // VPFormatsSupported is an object containing a list of key value pairs, where the key is a string identifying a Credential format supported by the Wallet.
+ VPFormatsSupported map[string]map[string][]string `json:"vp_formats_supported,omitempty"`
+
+ // VPFormats is an object containing a list of key value pairs, where the key is a string identifying a Credential format supported by the Verifier.
+ VPFormats map[string]map[string][]string `json:"vp_formats,omitempty"`
+
+ // ClientIdSchemesSupported defines the `client_id_schemes` currently supported.
+ // If omitted, the default value is `pre-registered` (referring to the client), which is currently not supported.
+ ClientIdSchemesSupported []string `json:"client_id_schemes_supported,omitempty"`
+}
+
+// ErrorResponse models an error returned from an OAuth flow according to RFC6749 (https://tools.ietf.org/html/rfc6749#page-45)
+type ErrorResponse struct {
+ Description *string `json:"error_description,omitempty"`
+ Error string `json:"error"`
+}
diff --git a/auth/services/messages.go b/auth/services/messages.go
index 7a19a288fa..e6d9858219 100644
--- a/auth/services/messages.go
+++ b/auth/services/messages.go
@@ -54,16 +54,6 @@ type CreateJwtGrantRequest struct {
Credentials []vc.VerifiableCredential
}
-// AccessTokenResult defines the return value back to the api for the CreateAccessToken method
-type AccessTokenResult struct {
- // AccessToken contains the JWT in compact serialization form
- AccessToken string `json:"access_token"`
- // ExpiresIn defines the expiration in seconds
- ExpiresIn int `json:"expires_in"`
- // TokenType The type of the token issued
- TokenType string `json:"token_type"`
-}
-
// JwtBearerTokenResult defines the return value back to the api for the createJwtBearerToken method
type JwtBearerTokenResult struct {
BearerToken string
diff --git a/auth/services/oauth/authz_server.go b/auth/services/oauth/authz_server.go
index 8db30970ef..4fd32ae59c 100644
--- a/auth/services/oauth/authz_server.go
+++ b/auth/services/oauth/authz_server.go
@@ -24,7 +24,6 @@ import (
"encoding/json"
"errors"
"fmt"
- "github.com/nuts-foundation/nuts-node/vdr/resolver"
"time"
"github.com/lestrrat-go/jwx/v2/jwt"
@@ -32,6 +31,7 @@ import (
vc2 "github.com/nuts-foundation/go-did/vc"
"github.com/nuts-foundation/nuts-node/auth/contract"
"github.com/nuts-foundation/nuts-node/auth/log"
+ "github.com/nuts-foundation/nuts-node/auth/oauth"
"github.com/nuts-foundation/nuts-node/auth/services"
"github.com/nuts-foundation/nuts-node/core"
nutsCrypto "github.com/nuts-foundation/nuts-node/crypto"
@@ -40,6 +40,7 @@ import (
"github.com/nuts-foundation/nuts-node/vcr"
"github.com/nuts-foundation/nuts-node/vcr/credential"
"github.com/nuts-foundation/nuts-node/vcr/verifier"
+ "github.com/nuts-foundation/nuts-node/vdr/resolver"
)
const errInvalidIssuerFmt = "invalid jwt.issuer: %w"
@@ -57,21 +58,6 @@ const secureAccessTokenLifeSpan = time.Minute
var _ AuthorizationServer = (*authzServer)(nil)
-// ErrorResponse models an error returned from an OAuth flow according to RFC6749 (https://tools.ietf.org/html/rfc6749#page-45)
-type ErrorResponse struct {
- Description error
- Code string
-}
-
-// Error returns the error detail, if any. If there's no detailed error message, it returns a generic error message.
-// This aids hiding internal errors from clients.
-func (e ErrorResponse) Error() string {
- if e.Description != nil {
- return e.Description.Error()
- }
- return "failed"
-}
-
type authzServer struct {
vcFinder vcr.Finder
vcVerifier verifier.Verifier
@@ -165,7 +151,7 @@ func (c validationContext) verifiableCredentials() ([]vc2.VerifiableCredential,
return vcs, nil
}
-// NewAuthorizationServer accepts a vendorID, and several Nuts engines and returns an implementation of services.AuthorizationServer
+// NewAuthorizationServer accepts a vendorID, and several Nuts engines and returns an implementation of services.OAuthAuthorizationServer
func NewAuthorizationServer(
didResolver resolver.DIDResolver, vcFinder vcr.Finder, vcVerifier verifier.Verifier,
serviceResolver didman.CompoundServiceResolver, privateKeyStore nutsCrypto.KeyStore,
@@ -197,27 +183,30 @@ func (s *authzServer) Configure(clockSkewInMilliseconds int, secureMode bool) er
}
// CreateAccessToken extracts the claims out of the request, checks the validity and builds the access token
-func (s *authzServer) CreateAccessToken(ctx context.Context, request services.CreateAccessTokenRequest) (*services.AccessTokenResult, *ErrorResponse) {
- var oauthError *ErrorResponse
- var result *services.AccessTokenResult
+func (s *authzServer) CreateAccessToken(ctx context.Context, request services.CreateAccessTokenRequest) (*oauth.TokenResponse, *oauth.ErrorResponse) {
+ var oauthError *oauth.ErrorResponse
+ var result *oauth.TokenResponse
validationCtx, err := s.validateAccessTokenRequest(ctx, request.RawJwtBearerToken)
if err != nil {
- oauthError = &ErrorResponse{Code: "invalid_request", Description: err}
+ errStr := err.Error()
+ oauthError = &oauth.ErrorResponse{Error: "invalid_request", Description: &errStr}
} else {
var accessToken string
var rawToken services.NutsAccessToken
accessToken, rawToken, err = s.buildAccessToken(ctx, *validationCtx.requester, *validationCtx.authorizer, validationCtx.purposeOfUse, validationCtx.contractVerificationResult, validationCtx.credentialIDs)
if err == nil {
- result = &services.AccessTokenResult{
+ expires := int(rawToken.Expiration - rawToken.IssuedAt)
+ result = &oauth.TokenResponse{
AccessToken: accessToken,
- ExpiresIn: int(rawToken.Expiration - rawToken.IssuedAt),
+ ExpiresIn: &expires,
}
} else {
- oauthError = &ErrorResponse{Code: "server_error"}
+ oauthError = &oauth.ErrorResponse{Error: "server_error"}
if !s.secureMode {
// Only set details when secure mode is disabled
- oauthError.Description = err
+ errStr := err.Error()
+ oauthError.Description = &errStr
}
}
}
diff --git a/auth/services/oauth/authz_server_test.go b/auth/services/oauth/authz_server_test.go
index c6373b28ff..4a78c2af73 100644
--- a/auth/services/oauth/authz_server_test.go
+++ b/auth/services/oauth/authz_server_test.go
@@ -116,7 +116,8 @@ func TestAuth_CreateAccessToken(t *testing.T) {
response, err := ctx.oauthService.CreateAccessToken(ctx.audit, services.CreateAccessTokenRequest{RawJwtBearerToken: "foo"})
assert.Nil(t, response)
- require.ErrorContains(t, err, "jwt bearer token validation failed")
+ require.NotNil(t, err.Description)
+ assert.Contains(t, *err.Description, "jwt bearer token validation failed")
})
t.Run("broken identity token", func(t *testing.T) {
@@ -133,7 +134,8 @@ func TestAuth_CreateAccessToken(t *testing.T) {
response, err := ctx.oauthService.CreateAccessToken(ctx.audit, services.CreateAccessTokenRequest{RawJwtBearerToken: tokenCtx.rawJwtBearerToken})
assert.Nil(t, response)
- require.ErrorContains(t, err, "identity validation failed")
+ require.NotNil(t, err.Description)
+ assert.Contains(t, *err.Description, "identity validation failed")
})
t.Run("JWT validity too long", func(t *testing.T) {
@@ -148,7 +150,8 @@ func TestAuth_CreateAccessToken(t *testing.T) {
response, err := ctx.oauthService.CreateAccessToken(ctx.audit, services.CreateAccessTokenRequest{RawJwtBearerToken: tokenCtx.rawJwtBearerToken})
assert.Nil(t, response)
- assert.ErrorContains(t, err, "JWT validity too long")
+ require.NotNil(t, err.Description)
+ assert.Contains(t, *err.Description, "JWT validity too long")
})
t.Run("invalid identity token", func(t *testing.T) {
@@ -166,7 +169,8 @@ func TestAuth_CreateAccessToken(t *testing.T) {
response, err := ctx.oauthService.CreateAccessToken(ctx.audit, services.CreateAccessTokenRequest{RawJwtBearerToken: tokenCtx.rawJwtBearerToken})
assert.Nil(t, response)
- assert.ErrorContains(t, err, "identity validation failed: because of reasons")
+ require.NotNil(t, err.Description)
+ assert.Contains(t, *err.Description, "identity validation failed: because of reasons")
})
t.Run("error detail masking", func(t *testing.T) {
@@ -195,9 +199,9 @@ func TestAuth_CreateAccessToken(t *testing.T) {
response, err := ctx.oauthService.CreateAccessToken(ctx.audit, services.CreateAccessTokenRequest{RawJwtBearerToken: tokenCtx.rawJwtBearerToken})
- require.Error(t, err)
assert.Nil(t, response)
- assert.EqualError(t, err, "could not build accessToken: signing error")
+ require.NotNil(t, err.Description)
+ assert.Contains(t, *err.Description, "could not build accessToken: signing error")
})
t.Run("mask internal errors when secureMode=true", func(t *testing.T) {
ctx := setup(createContext(t))
@@ -208,9 +212,9 @@ func TestAuth_CreateAccessToken(t *testing.T) {
response, err := ctx.oauthService.CreateAccessToken(ctx.audit, services.CreateAccessTokenRequest{RawJwtBearerToken: tokenCtx.rawJwtBearerToken})
- require.Error(t, err)
assert.Nil(t, response)
- assert.EqualError(t, err, "failed")
+ assert.Nil(t, err.Description)
+ assert.Equal(t, err.Error, "server_error")
})
})
@@ -272,7 +276,7 @@ func TestAuth_CreateAccessToken(t *testing.T) {
signToken(tokenCtx)
_, err := ctx.oauthService.CreateAccessToken(ctx.audit, services.CreateAccessTokenRequest{RawJwtBearerToken: tokenCtx.rawJwtBearerToken})
- require.Error(t, err)
+ require.NotNil(t, err)
})
}
diff --git a/auth/services/oauth/interface.go b/auth/services/oauth/interface.go
index e0d1dcf1d7..f393a2c4f2 100644
--- a/auth/services/oauth/interface.go
+++ b/auth/services/oauth/interface.go
@@ -20,15 +20,21 @@ package oauth
import (
"context"
+ "github.com/nuts-foundation/go-did/did"
+ "github.com/nuts-foundation/nuts-node/auth/oauth"
"github.com/nuts-foundation/nuts-node/auth/services"
"net/url"
)
// RelyingParty implements the OAuth2 relying party role.
type RelyingParty interface {
- // RequestAccessToken is called by the local EHR node to request an access token from a remote Nuts node.
- RequestAccessToken(ctx context.Context, jwtGrantToken string, authServerEndpoint url.URL) (*services.AccessTokenResult, error)
CreateJwtGrant(ctx context.Context, request services.CreateJwtGrantRequest) (*services.JwtBearerTokenResult, error)
+
+ // RequestRFC003AccessToken is called by the local EHR node to request an access token from a remote Nuts node using Nuts RFC003.
+ RequestRFC003AccessToken(ctx context.Context, jwtGrantToken string, authServerEndpoint url.URL) (*oauth.TokenResponse, error)
+
+ // RequestRFC021AccessToken is called by the local EHR node to request an access token from a remote Nuts node using Nuts RFC021.
+ RequestRFC021AccessToken(ctx context.Context, requestHolder did.DID, verifier did.DID, scopes string) (*oauth.TokenResponse, error)
}
// AuthorizationServer implements the OAuth2 authorization server role.
@@ -38,6 +44,6 @@ type AuthorizationServer interface {
// CreateAccessToken is called by remote Nuts nodes to create an access token,
// which can be used to access the local organization's XIS resources.
// It returns an oauth.ErrorResponse rather than a regular Go error, because the errors that may be returned are tightly specified.
- CreateAccessToken(ctx context.Context, request services.CreateAccessTokenRequest) (*services.AccessTokenResult, *ErrorResponse)
+ CreateAccessToken(ctx context.Context, request services.CreateAccessTokenRequest) (*oauth.TokenResponse, *oauth.ErrorResponse)
IntrospectAccessToken(ctx context.Context, token string) (*services.NutsAccessToken, error)
}
diff --git a/auth/services/oauth/mock.go b/auth/services/oauth/mock.go
index f65f49dc9c..8db473762a 100644
--- a/auth/services/oauth/mock.go
+++ b/auth/services/oauth/mock.go
@@ -13,6 +13,8 @@ import (
url "net/url"
reflect "reflect"
+ did "github.com/nuts-foundation/go-did/did"
+ oauth "github.com/nuts-foundation/nuts-node/auth/oauth"
services "github.com/nuts-foundation/nuts-node/auth/services"
gomock "go.uber.org/mock/gomock"
)
@@ -55,19 +57,34 @@ func (mr *MockRelyingPartyMockRecorder) CreateJwtGrant(ctx, request any) *gomock
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateJwtGrant", reflect.TypeOf((*MockRelyingParty)(nil).CreateJwtGrant), ctx, request)
}
-// RequestAccessToken mocks base method.
-func (m *MockRelyingParty) RequestAccessToken(ctx context.Context, jwtGrantToken string, authServerEndpoint url.URL) (*services.AccessTokenResult, error) {
+// RequestRFC003AccessToken mocks base method.
+func (m *MockRelyingParty) RequestRFC003AccessToken(ctx context.Context, jwtGrantToken string, authServerEndpoint url.URL) (*oauth.TokenResponse, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "RequestAccessToken", ctx, jwtGrantToken, authServerEndpoint)
- ret0, _ := ret[0].(*services.AccessTokenResult)
+ ret := m.ctrl.Call(m, "RequestRFC003AccessToken", ctx, jwtGrantToken, authServerEndpoint)
+ ret0, _ := ret[0].(*oauth.TokenResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// RequestAccessToken indicates an expected call of RequestAccessToken.
-func (mr *MockRelyingPartyMockRecorder) RequestAccessToken(ctx, jwtGrantToken, authServerEndpoint any) *gomock.Call {
+// RequestRFC003AccessToken indicates an expected call of RequestRFC003AccessToken.
+func (mr *MockRelyingPartyMockRecorder) RequestRFC003AccessToken(ctx, jwtGrantToken, authServerEndpoint any) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestAccessToken", reflect.TypeOf((*MockRelyingParty)(nil).RequestAccessToken), ctx, jwtGrantToken, authServerEndpoint)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestRFC003AccessToken", reflect.TypeOf((*MockRelyingParty)(nil).RequestRFC003AccessToken), ctx, jwtGrantToken, authServerEndpoint)
+}
+
+// RequestRFC021AccessToken mocks base method.
+func (m *MockRelyingParty) RequestRFC021AccessToken(ctx context.Context, requestHolder, verifier did.DID, scopes string) (*oauth.TokenResponse, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "RequestRFC021AccessToken", ctx, requestHolder, verifier, scopes)
+ ret0, _ := ret[0].(*oauth.TokenResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// RequestRFC021AccessToken indicates an expected call of RequestRFC021AccessToken.
+func (mr *MockRelyingPartyMockRecorder) RequestRFC021AccessToken(ctx, requestHolder, verifier, scopes any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestRFC021AccessToken", reflect.TypeOf((*MockRelyingParty)(nil).RequestRFC021AccessToken), ctx, requestHolder, verifier, scopes)
}
// MockAuthorizationServer is a mock of AuthorizationServer interface.
@@ -108,11 +125,11 @@ func (mr *MockAuthorizationServerMockRecorder) Configure(clockSkewInMilliseconds
}
// CreateAccessToken mocks base method.
-func (m *MockAuthorizationServer) CreateAccessToken(ctx context.Context, request services.CreateAccessTokenRequest) (*services.AccessTokenResult, *ErrorResponse) {
+func (m *MockAuthorizationServer) CreateAccessToken(ctx context.Context, request services.CreateAccessTokenRequest) (*oauth.TokenResponse, *oauth.ErrorResponse) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateAccessToken", ctx, request)
- ret0, _ := ret[0].(*services.AccessTokenResult)
- ret1, _ := ret[1].(*ErrorResponse)
+ ret0, _ := ret[0].(*oauth.TokenResponse)
+ ret1, _ := ret[1].(*oauth.ErrorResponse)
return ret0, ret1
}
diff --git a/auth/services/oauth/relying_party.go b/auth/services/oauth/relying_party.go
index 98d41b204b..6960bf6d21 100644
--- a/auth/services/oauth/relying_party.go
+++ b/auth/services/oauth/relying_party.go
@@ -21,8 +21,9 @@ package oauth
import (
"context"
"crypto/tls"
+ "errors"
"fmt"
- "github.com/nuts-foundation/nuts-node/vdr/resolver"
+ "github.com/nuts-foundation/nuts-node/openid4vc"
"net/http"
"net/url"
"strings"
@@ -31,11 +32,16 @@ import (
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/nuts-node/auth/api/auth/v1/client"
+ "github.com/nuts-foundation/nuts-node/auth/client/iam"
+ "github.com/nuts-foundation/nuts-node/auth/oauth"
"github.com/nuts-foundation/nuts-node/auth/services"
"github.com/nuts-foundation/nuts-node/core"
nutsCrypto "github.com/nuts-foundation/nuts-node/crypto"
"github.com/nuts-foundation/nuts-node/didman"
"github.com/nuts-foundation/nuts-node/vcr/credential"
+ "github.com/nuts-foundation/nuts-node/vcr/holder"
+ "github.com/nuts-foundation/nuts-node/vcr/signature/proof"
+ "github.com/nuts-foundation/nuts-node/vdr/resolver"
)
var _ RelyingParty = (*relyingParty)(nil)
@@ -44,51 +50,27 @@ type relyingParty struct {
keyResolver resolver.KeyResolver
privateKeyStore nutsCrypto.KeyStore
serviceResolver didman.CompoundServiceResolver
- secureMode bool
+ strictMode bool
httpClientTimeout time.Duration
httpClientTLS *tls.Config
+ wallet holder.Wallet
}
// NewRelyingParty returns an implementation of RelyingParty
func NewRelyingParty(
didResolver resolver.DIDResolver, serviceResolver didman.CompoundServiceResolver, privateKeyStore nutsCrypto.KeyStore,
- httpClientTimeout time.Duration, httpClientTLS *tls.Config) RelyingParty {
+ wallet holder.Wallet, httpClientTimeout time.Duration, httpClientTLS *tls.Config, strictMode bool) RelyingParty {
return &relyingParty{
keyResolver: resolver.DIDKeyResolver{Resolver: didResolver},
serviceResolver: serviceResolver,
privateKeyStore: privateKeyStore,
httpClientTimeout: httpClientTimeout,
httpClientTLS: httpClientTLS,
+ strictMode: strictMode,
+ wallet: wallet,
}
}
-// Configure the service
-func (s *relyingParty) Configure(secureMode bool) {
- s.secureMode = secureMode
-}
-
-// RequestAccessToken is called by the local EHR node to request an access token from a remote Nuts node.
-func (s *relyingParty) RequestAccessToken(ctx context.Context, jwtGrantToken string, authorizationServerEndpoint url.URL) (*services.AccessTokenResult, error) {
- if s.secureMode && strings.ToLower(authorizationServerEndpoint.Scheme) != "https" {
- return nil, fmt.Errorf("authorization server endpoint must be HTTPS when in strict mode: %s", authorizationServerEndpoint.String())
- }
- httpClient := &http.Client{}
- if s.httpClientTLS != nil {
- httpClient.Transport = &http.Transport{
- TLSClientConfig: s.httpClientTLS,
- }
- }
- authClient, err := client.NewHTTPClient("", s.httpClientTimeout, client.WithHTTPClient(httpClient), client.WithRequestEditorFn(core.UserAgentRequestEditor))
- if err != nil {
- return nil, fmt.Errorf("unable to create HTTP client: %w", err)
- }
- accessTokenResponse, err := authClient.CreateAccessToken(ctx, authorizationServerEndpoint, jwtGrantToken)
- if err != nil {
- return nil, fmt.Errorf("remote server/nuts node returned error creating access token: %w", err)
- }
- return accessTokenResponse, nil
-}
-
// CreateJwtGrant creates a JWT Grant from the given CreateJwtGrantRequest
func (s *relyingParty) CreateJwtGrant(ctx context.Context, request services.CreateJwtGrantRequest) (*services.JwtBearerTokenResult, error) {
requester, err := did.ParseDID(request.Requester)
@@ -128,6 +110,106 @@ func (s *relyingParty) CreateJwtGrant(ctx context.Context, request services.Crea
return &services.JwtBearerTokenResult{BearerToken: signingString, AuthorizationServerEndpoint: endpointURL}, nil
}
+func (s *relyingParty) RequestRFC003AccessToken(ctx context.Context, jwtGrantToken string, authorizationServerEndpoint url.URL) (*oauth.TokenResponse, error) {
+ if s.strictMode && strings.ToLower(authorizationServerEndpoint.Scheme) != "https" {
+ return nil, fmt.Errorf("authorization server endpoint must be HTTPS when in strict mode: %s", authorizationServerEndpoint.String())
+ }
+ httpClient := &http.Client{}
+ if s.httpClientTLS != nil {
+ httpClient.Transport = &http.Transport{
+ TLSClientConfig: s.httpClientTLS,
+ }
+ }
+ authClient, err := client.NewHTTPClient("", s.httpClientTimeout, client.WithHTTPClient(httpClient), client.WithRequestEditorFn(core.UserAgentRequestEditor))
+ if err != nil {
+ return nil, fmt.Errorf("unable to create HTTP client: %w", err)
+ }
+ accessTokenResponse, err := authClient.CreateAccessToken(ctx, authorizationServerEndpoint, jwtGrantToken)
+ if err != nil {
+ return nil, fmt.Errorf("remote server/nuts node returned error creating access token: %w", err)
+ }
+ return accessTokenResponse, nil
+}
+
+func (s *relyingParty) RequestRFC021AccessToken(ctx context.Context, requester did.DID, verifier did.DID, scopes string) (*oauth.TokenResponse, error) {
+ iamClient := iam.NewHTTPClient(s.strictMode, s.httpClientTimeout, s.httpClientTLS)
+ metadata, err := iamClient.OAuthAuthorizationServerMetadata(ctx, verifier)
+ if err != nil {
+ return nil, fmt.Errorf("failed to retrieve remote OAuth Authorization Server metadata: %w", err)
+ }
+
+ // get the presentation definition from the verifier
+ presentationDefinition, err := iamClient.PresentationDefinition(ctx, metadata.PresentationDefinitionEndpoint, scopes)
+ if err != nil {
+ return nil, fmt.Errorf("failed to retrieve presentation definition: %w", err)
+ }
+
+ walletCredentials, err := s.wallet.List(ctx, requester)
+ if err != nil {
+ return nil, fmt.Errorf("failed to retrieve wallet credentials: %w", err)
+ }
+
+ // match against the wallet's credentials
+ // if there's a match, create a VP and call the token endpoint
+ // If the token endpoint succeeds, return the access token
+ // If no presentation definition matches, return a 412 "no matching credentials" error
+ builder := presentationDefinition.PresentationSubmissionBuilder()
+ builder.AddWallet(requester, walletCredentials)
+ format, err := determineFormat(metadata.VPFormats)
+ if err != nil {
+ return nil, err
+ }
+ submission, signInstructions, err := builder.Build(format)
+ if err != nil {
+ return nil, fmt.Errorf("failed to match presentation definition: %w", err)
+ }
+ if signInstructions.Empty() {
+ return nil, core.Error(http.StatusPreconditionFailed, "no matching credentials")
+ }
+ expires := time.Now().Add(time.Minute * 15) //todo
+ nonce := nutsCrypto.GenerateNonce()
+ // todo: support multiple wallets
+ vp, err := s.wallet.BuildPresentation(ctx, signInstructions[0].VerifiableCredentials, holder.PresentationOptions{
+ Format: format,
+ ProofOptions: proof.ProofOptions{
+ Created: time.Now(),
+ Challenge: &nonce,
+ Expires: &expires,
+ },
+ }, &requester, false)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create verifiable presentation: %w", err)
+ }
+ token, err := iamClient.AccessToken(ctx, metadata.TokenEndpoint, *vp, submission, scopes)
+ if err != nil {
+ // the error could be a http error, we just relay it here to make use of any 400 status codes.
+ return nil, err
+ }
+ return &oauth.TokenResponse{
+ AccessToken: token.AccessToken,
+ ExpiresIn: token.ExpiresIn,
+ TokenType: token.TokenType,
+ Scope: &scopes,
+ }, nil
+}
+
+func determineFormat(formats map[string]map[string][]string) (string, error) {
+ for format := range formats {
+ switch format {
+ case openid4vc.VerifiablePresentationJWTFormat:
+ fallthrough
+ case openid4vc.VerifiablePresentationJSONLDFormat:
+ fallthrough
+ case "jwt_vp_json":
+ return format, nil
+ default:
+ continue
+ }
+ }
+
+ return "", errors.New("authorization server metadata does not contain any supported VP formats")
+}
+
var timeFunc = time.Now
// standalone func for easier testing
diff --git a/auth/services/oauth/relying_party_test.go b/auth/services/oauth/relying_party_test.go
index 7cd31b6837..2f0e828400 100644
--- a/auth/services/oauth/relying_party_test.go
+++ b/auth/services/oauth/relying_party_test.go
@@ -21,11 +21,13 @@ package oauth
import (
"context"
"crypto/tls"
+ "encoding/json"
"errors"
"fmt"
"github.com/nuts-foundation/nuts-node/audit"
+ "github.com/nuts-foundation/nuts-node/auth/oauth"
+ "github.com/nuts-foundation/nuts-node/core"
http2 "github.com/nuts-foundation/nuts-node/test/http"
- "github.com/nuts-foundation/nuts-node/vdr/resolver"
"net/http"
"net/http/httptest"
"net/url"
@@ -38,45 +40,49 @@ import (
"github.com/nuts-foundation/nuts-node/auth/services"
"github.com/nuts-foundation/nuts-node/crypto"
"github.com/nuts-foundation/nuts-node/didman"
+ vcr "github.com/nuts-foundation/nuts-node/vcr/api/vcr/v2"
"github.com/nuts-foundation/nuts-node/vcr/credential"
+ "github.com/nuts-foundation/nuts-node/vcr/holder"
"github.com/nuts-foundation/nuts-node/vdr"
+ "github.com/nuts-foundation/nuts-node/vdr/didweb"
+ "github.com/nuts-foundation/nuts-node/vdr/resolver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
-func TestRelyingParty_RequestAccessToken(t *testing.T) {
+func TestRelyingParty_RequestRFC003AccessToken(t *testing.T) {
const bearerToken = "jwt-bearer-token"
t.Run("ok", func(t *testing.T) {
- ctx := createRPContext(t)
+ ctx := createRPContext(t, nil)
httpHandler := &http2.Handler{
StatusCode: http.StatusOK,
}
httpServer := httptest.NewServer(httpHandler)
t.Cleanup(httpServer.Close)
- response, err := ctx.relyingParty.RequestAccessToken(context.Background(), bearerToken, mustParseURL(httpServer.URL))
+ response, err := ctx.relyingParty.RequestRFC003AccessToken(context.Background(), bearerToken, mustParseURL(httpServer.URL))
assert.NoError(t, err)
assert.NotNil(t, response)
assert.Equal(t, "nuts-node-refimpl/unknown", httpHandler.RequestHeaders.Get("User-Agent"))
})
t.Run("returns error when HTTP create access token fails", func(t *testing.T) {
- ctx := createRPContext(t)
+ ctx := createRPContext(t, nil)
server := httptest.NewServer(&http2.Handler{
StatusCode: http.StatusBadGateway,
})
t.Cleanup(server.Close)
- response, err := ctx.relyingParty.RequestAccessToken(context.Background(), bearerToken, mustParseURL(server.URL))
+ response, err := ctx.relyingParty.RequestRFC003AccessToken(context.Background(), bearerToken, mustParseURL(server.URL))
assert.Nil(t, response)
assert.EqualError(t, err, "remote server/nuts node returned error creating access token: server returned HTTP 502 (expected: 200)")
})
t.Run("endpoint security validation (only HTTPS in strict mode)", func(t *testing.T) {
- ctx := createRPContext(t)
+ ctx := createRPContext(t, nil)
httpServer := httptest.NewServer(&http2.Handler{
StatusCode: http.StatusOK,
})
@@ -87,25 +93,25 @@ func TestRelyingParty_RequestAccessToken(t *testing.T) {
t.Cleanup(httpsServer.Close)
t.Run("HTTPS in strict mode", func(t *testing.T) {
- ctx.relyingParty.secureMode = true
+ ctx.relyingParty.strictMode = true
- response, err := ctx.relyingParty.RequestAccessToken(context.Background(), bearerToken, mustParseURL(httpsServer.URL))
+ response, err := ctx.relyingParty.RequestRFC003AccessToken(context.Background(), bearerToken, mustParseURL(httpsServer.URL))
assert.NoError(t, err)
assert.NotNil(t, response)
})
t.Run("HTTP allowed in non-strict mode", func(t *testing.T) {
- ctx.relyingParty.secureMode = false
+ ctx.relyingParty.strictMode = false
- response, err := ctx.relyingParty.RequestAccessToken(context.Background(), bearerToken, mustParseURL(httpServer.URL))
+ response, err := ctx.relyingParty.RequestRFC003AccessToken(context.Background(), bearerToken, mustParseURL(httpServer.URL))
assert.NoError(t, err)
assert.NotNil(t, response)
})
t.Run("HTTP not allowed in strict mode", func(t *testing.T) {
- ctx.relyingParty.secureMode = true
+ ctx.relyingParty.strictMode = true
- response, err := ctx.relyingParty.RequestAccessToken(context.Background(), bearerToken, mustParseURL(httpServer.URL))
+ response, err := ctx.relyingParty.RequestRFC003AccessToken(context.Background(), bearerToken, mustParseURL(httpServer.URL))
assert.EqualError(t, err, fmt.Sprintf("authorization server endpoint must be HTTPS when in strict mode: %s", httpServer.URL))
assert.Nil(t, response)
@@ -113,6 +119,99 @@ func TestRelyingParty_RequestAccessToken(t *testing.T) {
})
}
+func TestRelyingParty_RequestRFC021AccessToken(t *testing.T) {
+ walletDID := did.MustParseDID("did:test:123")
+ scopes := "first second"
+ credentials := []vcr.VerifiableCredential{credential.ValidNutsOrganizationCredential(t)}
+
+ t.Run("ok", func(t *testing.T) {
+ ctx := createOAuthRPContext(t)
+ ctx.wallet.EXPECT().List(gomock.Any(), walletDID).Return(credentials, nil)
+ ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), credentials, gomock.Any(), &walletDID, false).Return(&vc.VerifiablePresentation{}, nil).Return(&vc.VerifiablePresentation{}, nil)
+
+ response, err := ctx.relyingParty.RequestRFC021AccessToken(context.Background(), walletDID, ctx.verifierDID, scopes)
+
+ assert.NoError(t, err)
+ require.NotNil(t, response)
+ assert.Equal(t, "token", response.AccessToken)
+ assert.Equal(t, "bearer", response.TokenType)
+ })
+ t.Run("error - access denied", func(t *testing.T) {
+ oauthError := oauth.OAuth2Error{
+ Code: "invalid_scope",
+ Description: "the scope you requested is unknown",
+ }
+ oauthErrorBytes, _ := json.Marshal(oauthError)
+ ctx := createOAuthRPContext(t)
+ ctx.token = func(writer http.ResponseWriter) {
+ writer.Header().Add("Content-Type", "application/json")
+ writer.WriteHeader(http.StatusBadRequest)
+ _, _ = writer.Write(oauthErrorBytes)
+ }
+ ctx.wallet.EXPECT().List(gomock.Any(), walletDID).Return(credentials, nil)
+ ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), credentials, gomock.Any(), &walletDID, false).Return(&vc.VerifiablePresentation{}, nil).Return(&vc.VerifiablePresentation{}, nil)
+
+ _, err := ctx.relyingParty.RequestRFC021AccessToken(context.Background(), walletDID, ctx.verifierDID, scopes)
+
+ require.Error(t, err)
+ httpError, ok := err.(core.HttpError)
+ require.True(t, ok)
+ assert.Equal(t, http.StatusBadRequest, httpError.StatusCode)
+ assert.Equal(t, oauthErrorBytes, httpError.ResponseBody)
+ })
+ t.Run("error - no matching credentials", func(t *testing.T) {
+ ctx := createOAuthRPContext(t)
+ ctx.wallet.EXPECT().List(gomock.Any(), walletDID).Return([]vcr.VerifiableCredential{}, nil)
+
+ _, err := ctx.relyingParty.RequestRFC021AccessToken(context.Background(), walletDID, ctx.verifierDID, scopes)
+
+ assert.Error(t, err)
+ // the error should be a 412 precondition failed
+ assert.EqualError(t, err, "no matching credentials")
+ })
+ t.Run("error - failed to get presentation definition", func(t *testing.T) {
+ ctx := createOAuthRPContext(t)
+ ctx.presentationDefinition = nil
+
+ _, err := ctx.relyingParty.RequestRFC021AccessToken(context.Background(), walletDID, ctx.verifierDID, scopes)
+
+ assert.Error(t, err)
+ assert.EqualError(t, err, "failed to retrieve presentation definition: server returned HTTP 404 (expected: 200)")
+ })
+ t.Run("error - failed to get authorization server metadata", func(t *testing.T) {
+ ctx := createOAuthRPContext(t)
+ ctx.metadata = nil
+
+ _, err := ctx.relyingParty.RequestRFC021AccessToken(context.Background(), walletDID, ctx.verifierDID, scopes)
+
+ assert.Error(t, err)
+ assert.EqualError(t, err, "failed to retrieve remote OAuth Authorization Server metadata: server returned HTTP 404 (expected: 200)")
+ })
+ t.Run("error - faulty presentation definition", func(t *testing.T) {
+ ctx := createOAuthRPContext(t)
+ ctx.presentationDefinition = func(writer http.ResponseWriter) {
+ writer.Header().Add("Content-Type", "application/json")
+ writer.WriteHeader(http.StatusOK)
+ _, _ = writer.Write([]byte("{"))
+ }
+
+ _, err := ctx.relyingParty.RequestRFC021AccessToken(context.Background(), walletDID, ctx.verifierDID, scopes)
+
+ assert.Error(t, err)
+ assert.EqualError(t, err, "failed to retrieve presentation definition: unable to unmarshal response: unexpected end of JSON input")
+ })
+ t.Run("error - failed to build vp", func(t *testing.T) {
+ ctx := createOAuthRPContext(t)
+ ctx.wallet.EXPECT().List(gomock.Any(), walletDID).Return(credentials, nil)
+ ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), credentials, gomock.Any(), &walletDID, false).Return(&vc.VerifiablePresentation{}, nil).Return(nil, errors.New("error"))
+
+ _, err := ctx.relyingParty.RequestRFC021AccessToken(context.Background(), walletDID, ctx.verifierDID, scopes)
+
+ assert.Error(t, err)
+ assert.EqualError(t, err, "failed to create verifiable presentation: error")
+ })
+}
+
func TestService_CreateJwtBearerToken(t *testing.T) {
usi := vc.VerifiablePresentation{}
@@ -146,7 +245,7 @@ func TestService_CreateJwtBearerToken(t *testing.T) {
}
t.Run("create a JwtBearerToken", func(t *testing.T) {
- ctx := createRPContext(t)
+ ctx := createRPContext(t, nil)
ctx.didResolver.EXPECT().Resolve(authorizerDID, gomock.Any()).Return(authorizerDIDDocument, nil, nil).AnyTimes()
ctx.serviceResolver.EXPECT().GetCompoundServiceEndpoint(authorizerDID, expectedService, services.OAuthEndpointType, true).Return(expectedAudience, nil)
@@ -163,7 +262,7 @@ func TestService_CreateJwtBearerToken(t *testing.T) {
})
t.Run("create a JwtBearerToken with valid credentials", func(t *testing.T) {
- ctx := createRPContext(t)
+ ctx := createRPContext(t, nil)
ctx.didResolver.EXPECT().Resolve(authorizerDID, gomock.Any()).Return(authorizerDIDDocument, nil, nil).AnyTimes()
ctx.serviceResolver.EXPECT().GetCompoundServiceEndpoint(authorizerDID, expectedService, services.OAuthEndpointType, true).Return(expectedAudience, nil)
@@ -180,7 +279,7 @@ func TestService_CreateJwtBearerToken(t *testing.T) {
})
t.Run("create a JwtBearerToken with invalid credentials fails", func(t *testing.T) {
- ctx := createRPContext(t)
+ ctx := createRPContext(t, nil)
invalidCredential := validCredential
invalidCredential.Type = []ssi.URI{}
@@ -197,7 +296,7 @@ func TestService_CreateJwtBearerToken(t *testing.T) {
})
t.Run("authorizer without endpoint", func(t *testing.T) {
- ctx := createRPContext(t)
+ ctx := createRPContext(t, nil)
document := getAuthorizerDIDDocument()
document.Service = []did.Service{}
@@ -210,7 +309,7 @@ func TestService_CreateJwtBearerToken(t *testing.T) {
})
t.Run("request without authorizer", func(t *testing.T) {
- ctx := createRPContext(t)
+ ctx := createRPContext(t, nil)
request := services.CreateJwtGrantRequest{
Requester: requesterDID.String(),
@@ -224,7 +323,7 @@ func TestService_CreateJwtBearerToken(t *testing.T) {
})
t.Run("signing error", func(t *testing.T) {
- ctx := createRPContext(t)
+ ctx := createRPContext(t, nil)
ctx.didResolver.EXPECT().Resolve(authorizerDID, gomock.Any()).Return(authorizerDIDDocument, nil, nil).AnyTimes()
ctx.serviceResolver.EXPECT().GetCompoundServiceEndpoint(authorizerDID, expectedService, services.OAuthEndpointType, true).Return(expectedAudience, nil)
@@ -238,16 +337,6 @@ func TestService_CreateJwtBearerToken(t *testing.T) {
})
}
-func TestRelyingParty_Configure(t *testing.T) {
- t.Run("ok - config valid", func(t *testing.T) {
- ctx := createRPContext(t)
-
- ctx.relyingParty.Configure(true)
-
- assert.True(t, ctx.relyingParty.secureMode)
- })
-}
-
type rpTestContext struct {
ctrl *gomock.Controller
keyStore *crypto.MockKeyStore
@@ -256,32 +345,128 @@ type rpTestContext struct {
serviceResolver *didman.MockCompoundServiceResolver
relyingParty *relyingParty
audit context.Context
+ wallet *holder.MockWallet
}
-var createRPContext = func(t *testing.T) *rpTestContext {
+func createRPContext(t *testing.T, tlsConfig *tls.Config) *rpTestContext {
ctrl := gomock.NewController(t)
privateKeyStore := crypto.NewMockKeyStore(ctrl)
keyResolver := resolver.NewMockKeyResolver(ctrl)
serviceResolver := didman.NewMockCompoundServiceResolver(ctrl)
didResolver := resolver.NewMockDIDResolver(ctrl)
+ wallet := holder.NewMockWallet(ctrl)
+
+ if tlsConfig == nil {
+ tlsConfig = &tls.Config{}
+ }
+ tlsConfig.InsecureSkipVerify = true
return &rpTestContext{
- ctrl: ctrl,
- keyStore: privateKeyStore,
- keyResolver: keyResolver,
- serviceResolver: serviceResolver,
- didResolver: didResolver,
+ audit: audit.TestContext(),
+ ctrl: ctrl,
+ didResolver: didResolver,
+ keyStore: privateKeyStore,
+ keyResolver: keyResolver,
relyingParty: &relyingParty{
+ httpClientTLS: tlsConfig,
keyResolver: keyResolver,
privateKeyStore: privateKeyStore,
serviceResolver: serviceResolver,
- httpClientTLS: &tls.Config{
- InsecureSkipVerify: true,
- },
+ wallet: wallet,
+ },
+ serviceResolver: serviceResolver,
+ wallet: wallet,
+ }
+}
+
+type rpOAuthTestContext struct {
+ *rpTestContext
+ authzServerMetadata oauth.AuthorizationServerMetadata
+ handler http.HandlerFunc
+ tlsServer *httptest.Server
+ verifierDID did.DID
+ metadata func(writer http.ResponseWriter)
+ presentationDefinition func(writer http.ResponseWriter)
+ token func(writer http.ResponseWriter)
+}
+
+func createOAuthRPContext(t *testing.T) *rpOAuthTestContext {
+ presentationDefinition := `
+{
+ "input_descriptors": [
+ {
+ "name": "Pick 1",
+ "group": ["A"],
+ "constraints": {
+ "fields": [
+ {
+ "path": [
+ "$.type"
+ ],
+ "filter": {
+ "type": "string",
+ "const": "NutsOrganizationCredential"
+ }
+ }
+ ]
+ }
+ }
+ ]
+}
+`
+ formats := make(map[string]map[string][]string)
+ formats["jwt_vp"] = make(map[string][]string)
+ authzServerMetadata := oauth.AuthorizationServerMetadata{VPFormats: formats}
+ ctx := &rpOAuthTestContext{
+ rpTestContext: createRPContext(t, nil),
+ metadata: func(writer http.ResponseWriter) {
+ writer.Header().Add("Content-Type", "application/json")
+ writer.WriteHeader(http.StatusOK)
+ bytes, _ := json.Marshal(authzServerMetadata)
+ _, _ = writer.Write(bytes)
+ return
+ },
+ presentationDefinition: func(writer http.ResponseWriter) {
+ writer.Header().Add("Content-Type", "application/json")
+ writer.WriteHeader(http.StatusOK)
+ _, _ = writer.Write([]byte(presentationDefinition))
+ return
+ },
+ token: func(writer http.ResponseWriter) {
+ writer.Header().Add("Content-Type", "application/json")
+ writer.WriteHeader(http.StatusOK)
+ _, _ = writer.Write([]byte(`{"access_token": "token", "token_type": "bearer"}`))
+ return
},
- audit: audit.TestContext(),
}
+ ctx.handler = func(writer http.ResponseWriter, request *http.Request) {
+ switch request.URL.Path {
+ case "/.well-known/oauth-authorization-server":
+ if ctx.metadata != nil {
+ ctx.metadata(writer)
+ return
+ }
+ case "/presentation_definition":
+ if ctx.presentationDefinition != nil {
+ ctx.presentationDefinition(writer)
+ return
+ }
+ case "/token":
+ if ctx.token != nil {
+ ctx.token(writer)
+ return
+ }
+ }
+ writer.WriteHeader(http.StatusNotFound)
+ }
+ ctx.tlsServer = http2.TestTLSServer(t, ctx.handler)
+ ctx.verifierDID = didweb.ServerURLToDIDWeb(t, ctx.tlsServer.URL)
+ authzServerMetadata.TokenEndpoint = ctx.tlsServer.URL + "/token"
+ authzServerMetadata.PresentationDefinitionEndpoint = ctx.tlsServer.URL + "/presentation_definition"
+ ctx.authzServerMetadata = authzServerMetadata
+
+ return ctx
}
func mustParseURL(str string) url.URL {
diff --git a/codegen/configs/auth_client_v1.yaml b/codegen/configs/auth_client_v1.yaml
index 7280dc1594..7629aef716 100644
--- a/codegen/configs/auth_client_v1.yaml
+++ b/codegen/configs/auth_client_v1.yaml
@@ -8,3 +8,4 @@ output-options:
- VerifiableCredential
- VerifiablePresentation
- AccessTokenResponse
+ - AccessTokenRequestFailedResponse
diff --git a/codegen/configs/auth_iam.yaml b/codegen/configs/auth_iam.yaml
index 0766286caa..86fc29a911 100644
--- a/codegen/configs/auth_iam.yaml
+++ b/codegen/configs/auth_iam.yaml
@@ -9,5 +9,5 @@ output-options:
- DIDDocument
- OAuthAuthorizationServerMetadata
- OAuthClientMetadata
- - ErrorResponse
- PresentationDefinition
+ - TokenResponse
diff --git a/codegen/configs/auth_v1.yaml b/codegen/configs/auth_v1.yaml
index 23b92666fc..25ee1db8ae 100644
--- a/codegen/configs/auth_v1.yaml
+++ b/codegen/configs/auth_v1.yaml
@@ -10,3 +10,4 @@ output-options:
- VerifiableCredential
- VerifiablePresentation
- AccessTokenResponse
+ - AccessTokenRequestFailedResponse
diff --git a/core/http_client.go b/core/http_client.go
index 3787771653..a89a093552 100644
--- a/core/http_client.go
+++ b/core/http_client.go
@@ -21,10 +21,12 @@ package core
import (
"context"
+ "crypto/tls"
"errors"
"fmt"
"io"
"net/http"
+ "time"
)
// HttpError describes an error returned when invoking a remote server.
@@ -140,20 +142,38 @@ func newEmptyTokenGenerator() AuthorizationTokenGenerator {
}
// NewStrictHTTPClient creates a HTTPRequestDoer that only allows HTTPS calls when strictmode is enabled.
-func NewStrictHTTPClient(strictmode bool, client *http.Client) HTTPRequestDoer {
- return &strictHTTPClient{
- client: client,
- strictmode: strictmode,
+func NewStrictHTTPClient(strictmode bool, timeout time.Duration, tlsConfig *tls.Config) *StrictHTTPClient {
+ if tlsConfig == nil {
+ tlsConfig = &tls.Config{
+ MinVersion: tls.VersionTLS12,
+ }
+ }
+
+ transport := http.DefaultTransport
+ // Might not be http.Transport in testing
+ if httpTransport, ok := transport.(*http.Transport); ok {
+ // cloning the transport might reduce performance.
+ httpTransport = httpTransport.Clone()
+ httpTransport.TLSClientConfig = tlsConfig
+ transport = httpTransport
+ }
+
+ return &StrictHTTPClient{
+ client: &http.Client{
+ Transport: transport,
+ Timeout: timeout,
+ },
+ strictMode: strictmode,
}
}
-type strictHTTPClient struct {
+type StrictHTTPClient struct {
client *http.Client
- strictmode bool
+ strictMode bool
}
-func (s *strictHTTPClient) Do(req *http.Request) (*http.Response, error) {
- if s.strictmode && req.URL.Scheme != "https" {
+func (s *StrictHTTPClient) Do(req *http.Request) (*http.Response, error) {
+ if s.strictMode && req.URL.Scheme != "https" {
return nil, errors.New("strictmode is enabled, but request is not over HTTPS")
}
return s.client.Do(req)
diff --git a/core/http_client_test.go b/core/http_client_test.go
index 4dc5dae438..8bf0c77929 100644
--- a/core/http_client_test.go
+++ b/core/http_client_test.go
@@ -27,6 +27,7 @@ import (
stdHttp "net/http"
"net/http/httptest"
"testing"
+ "time"
)
func TestHTTPClient(t *testing.T) {
@@ -89,7 +90,7 @@ func TestHTTPClient(t *testing.T) {
func TestStrictHTTPClient_Do(t *testing.T) {
t.Run("error on HTTP call when strictmode is enabled", func(t *testing.T) {
- client := NewStrictHTTPClient(true, &stdHttp.Client{})
+ client := NewStrictHTTPClient(true, time.Second, nil)
httpRequest, _ := stdHttp.NewRequest("GET", "http://example.com", nil)
_, err := client.Do(httpRequest)
diff --git a/crypto/random.go b/crypto/random.go
new file mode 100644
index 0000000000..0c79728be4
--- /dev/null
+++ b/crypto/random.go
@@ -0,0 +1,34 @@
+/*
+ * Nuts node
+ * Copyright (C) 2023 Nuts community
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package crypto
+
+import (
+ "crypto/rand"
+ "encoding/base64"
+)
+
+// GenerateNonce creates a 128 bit secure random
+func GenerateNonce() string {
+ buf := make([]byte, 128/8)
+ _, err := rand.Read(buf)
+ if err != nil {
+ panic(err)
+ }
+ return base64.RawURLEncoding.EncodeToString(buf)
+}
diff --git a/auth/types.go b/crypto/random_test.go
similarity index 69%
rename from auth/types.go
rename to crypto/random_test.go
index ae212a0d56..91e7afcb27 100644
--- a/auth/types.go
+++ b/crypto/random_test.go
@@ -1,5 +1,6 @@
/*
- * Copyright (C) 2021 Nuts community
+ * Nuts node
+ * Copyright (C) 2023 Nuts community
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -13,10 +14,20 @@
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
- *
*/
-package auth
+package crypto
+
+import (
+ "encoding/base64"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRandom(t *testing.T) {
+ nonce := GenerateNonce()
+ decoded, _ := base64.RawURLEncoding.DecodeString(nonce)
-// ModuleName contains the name of this module
-const ModuleName = "Auth"
+ assert.Len(t, decoded, 16)
+}
diff --git a/docs/_static/auth/iam.yaml b/docs/_static/auth/iam.yaml
index 59a5ba762f..977f7303e6 100644
--- a/docs/_static/auth/iam.yaml
+++ b/docs/_static/auth/iam.yaml
@@ -69,20 +69,8 @@ paths:
application/json:
schema:
"$ref": "#/components/schemas/TokenResponse"
- "404":
- description: Unknown issuer
- content:
- application/json:
- schema:
- "$ref": "#/components/schemas/ErrorResponse"
- "400":
- description: >
- Invalid request. Code can be "invalid_request", "invalid_client", "invalid_grant", "unauthorized_client", "unsupported_grant_type" or "invalid_scope".
- Specified by https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-token-error-response
- content:
- application/json:
- schema:
- "$ref": "#/components/schemas/ErrorResponse"
+ "default":
+ $ref: '../common/error_response.yaml'
"/iam/{id}/authorize":
get:
summary: Used by resource owners to initiate the authorization code flow.
@@ -156,14 +144,8 @@ paths:
application/json:
schema:
"$ref": "#/components/schemas/PresentationDefinition"
- "400":
- description: invalid scope
- content:
- application/json:
- schema:
- "$ref": "#/components/schemas/ErrorResponse"
- "404":
- description: Unknown DID
+ "default":
+ $ref: '../common/error_response.yaml'
# TODO: What format to use? (codegenerator breaks on aliases)
# See issue https://github.com/nuts-foundation/nuts-node/issues/2365
# create aliases for the specced path
@@ -292,6 +274,7 @@ paths:
error returns:
* 400 - one of the parameters has the wrong format
+ * 412 - the organization wallet does not contain the correct credentials
* 503 - the authorizer could not be reached or returned an error
tags:
- auth
@@ -377,13 +360,3 @@ components:
description: |
A presentation definition is a JSON object that describes the desired verifiable credentials and presentation formats.
Specified at https://identity.foundation/presentation-exchange/spec/v2.0.0/
- type: object
- ErrorResponse:
- type: object
- required:
- - error
- properties:
- error:
- type: string
- description: Code identifying the error that occurred.
- example: "invalid_request"
diff --git a/openid4vc/types.go b/openid4vc/types.go
new file mode 100644
index 0000000000..b75973668e
--- /dev/null
+++ b/openid4vc/types.go
@@ -0,0 +1,29 @@
+/*
+ * Nuts node
+ * Copyright (C) 2021 Nuts community
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+// openid4vc contains common constants and logic for OpenID4VCI, SiopV2 and OpenID4VP
+package openid4vc
+
+// VerifiableCredentialJSONLDFormat defines the JSON-LD format identifier for Verifiable Credentials.
+const VerifiableCredentialJSONLDFormat = "ldp_vc"
+
+// VerifiablePresentationJSONLDFormat defines the JSON-LD format identifier for Verifiable Presentations.
+const VerifiablePresentationJSONLDFormat = "ldp_vp"
+
+// VerifiablePresentationJWTFormat defines the JWT format identifier for Verifiable Presentations.
+const VerifiablePresentationJWTFormat = "jwt_vp"
diff --git a/vcr/ambassador_test.go b/vcr/ambassador_test.go
index 11f25f5fb6..aedb6b979b 100644
--- a/vcr/ambassador_test.go
+++ b/vcr/ambassador_test.go
@@ -107,9 +107,7 @@ func TestAmbassador_handleReprocessEvent(t *testing.T) {
ctx.vcr.ambassador.(*ambassador).writer = mockWriter
// load VC
- vc := vc.VerifiableCredential{}
- vcJSON, _ := os.ReadFile("test/vc.json")
- json.Unmarshal(vcJSON, &vc)
+ vc := credential.ValidNutsOrganizationCredential(t)
// load key
pem, _ := os.ReadFile("test/private.pem")
diff --git a/vcr/api/openid4vci/v0/api.go b/vcr/api/openid4vci/v0/api.go
index 432a5e4c7b..209ebb467d 100644
--- a/vcr/api/openid4vci/v0/api.go
+++ b/vcr/api/openid4vci/v0/api.go
@@ -24,6 +24,7 @@ import (
"github.com/labstack/echo/v4"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/nuts-node/audit"
+ "github.com/nuts-foundation/nuts-node/auth/oauth"
"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/vcr"
"github.com/nuts-foundation/nuts-node/vcr/log"
@@ -39,7 +40,7 @@ type ProviderMetadata = openid4vci.ProviderMetadata
type CredentialIssuerMetadata = openid4vci.CredentialIssuerMetadata
// TokenResponse is the response of the OpenID Connect token endpoint
-type TokenResponse = openid4vci.TokenResponse
+type TokenResponse = oauth.TokenResponse
// CredentialOfferResponse is the response to the OpenID4VCI credential offer
type CredentialOfferResponse = openid4vci.CredentialOfferResponse
diff --git a/vcr/api/openid4vci/v0/holder_test.go b/vcr/api/openid4vci/v0/holder_test.go
index 622d9e044c..80b74fd29f 100644
--- a/vcr/api/openid4vci/v0/holder_test.go
+++ b/vcr/api/openid4vci/v0/holder_test.go
@@ -23,6 +23,7 @@ import (
"encoding/json"
ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/go-did/did"
+ "github.com/nuts-foundation/nuts-node/openid4vc"
"github.com/nuts-foundation/nuts-node/vcr"
"github.com/nuts-foundation/nuts-node/vcr/holder"
"github.com/nuts-foundation/nuts-node/vcr/openid4vci"
@@ -84,7 +85,7 @@ func TestWrapper_HandleCredentialOffer(t *testing.T) {
CredentialIssuer: issuerDID.String(),
Credentials: []openid4vci.OfferedCredential{
{
- Format: openid4vci.VerifiableCredentialJSONLDFormat,
+ Format: openid4vc.VerifiableCredentialJSONLDFormat,
CredentialDefinition: &openid4vci.CredentialDefinition{
Context: []ssi.URI{ssi.MustParseURI("a"), ssi.MustParseURI("b")},
Type: []ssi.URI{ssi.MustParseURI("VerifiableCredential"), ssi.MustParseURI("HumanCredential")},
diff --git a/vcr/api/openid4vci/v0/issuer.go b/vcr/api/openid4vci/v0/issuer.go
index 292b9eda05..77e3c24731 100644
--- a/vcr/api/openid4vci/v0/issuer.go
+++ b/vcr/api/openid4vci/v0/issuer.go
@@ -23,6 +23,7 @@ import (
"encoding/json"
"errors"
"fmt"
+ "github.com/nuts-foundation/nuts-node/openid4vc"
"github.com/nuts-foundation/nuts-node/vcr/issuer"
"github.com/nuts-foundation/nuts-node/vcr/openid4vci"
"net/http"
@@ -107,7 +108,7 @@ func (w Wrapper) RequestCredential(ctx context.Context, request RequestCredentia
}
return RequestCredential200JSONResponse(CredentialResponse{
Credential: &credentialMap,
- Format: openid4vci.VerifiableCredentialJSONLDFormat,
+ Format: openid4vc.VerifiableCredentialJSONLDFormat,
}), nil
}
@@ -129,10 +130,11 @@ func (w Wrapper) RequestAccessToken(ctx context.Context, request RequestAccessTo
if err != nil {
return nil, err
}
+ expiresIn := int(issuer.TokenTTL.Seconds())
return RequestAccessToken200JSONResponse(TokenResponse{
AccessToken: accessToken,
- CNonce: cNonce,
- ExpiresIn: int(issuer.TokenTTL.Seconds()),
+ CNonce: &cNonce,
+ ExpiresIn: &expiresIn,
TokenType: "bearer",
}), nil
}
diff --git a/vcr/api/openid4vci/v0/issuer_test.go b/vcr/api/openid4vci/v0/issuer_test.go
index 1758789dbd..13fa3b2f04 100644
--- a/vcr/api/openid4vci/v0/issuer_test.go
+++ b/vcr/api/openid4vci/v0/issuer_test.go
@@ -116,7 +116,7 @@ func TestWrapper_RequestAccessToken(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "access-token", response.(RequestAccessToken200JSONResponse).AccessToken)
- assert.Equal(t, "c_nonce", response.(RequestAccessToken200JSONResponse).CNonce)
+ assert.Equal(t, "c_nonce", *response.(RequestAccessToken200JSONResponse).CNonce)
})
t.Run("unknown tenant", func(t *testing.T) {
ctrl := gomock.NewController(t)
diff --git a/vcr/test/vc.json b/vcr/assets/test_assets/vc.json
similarity index 100%
rename from vcr/test/vc.json
rename to vcr/assets/test_assets/vc.json
diff --git a/vcr/context_test.go b/vcr/context_test.go
index b29d06d096..332bc01271 100644
--- a/vcr/context_test.go
+++ b/vcr/context_test.go
@@ -22,11 +22,11 @@ package vcr
import (
"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/jsonld"
+ "github.com/nuts-foundation/nuts-node/vcr/assets"
"github.com/nuts-foundation/nuts-node/vcr/credential"
"github.com/piprate/json-gold/ld"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "os"
"testing"
)
@@ -37,7 +37,7 @@ func TestNutsV1Context(t *testing.T) {
reader := jsonld.Reader{DocumentLoader: jsonldManager.DocumentLoader()}
t.Run("NutsOrganizationCredential", func(t *testing.T) {
- vcJSON, _ := os.ReadFile("test/vc.json")
+ vcJSON, _ := assets.TestAssets.ReadFile("test_assets/vc.json")
documents, err := reader.ReadBytes(vcJSON)
if err != nil {
panic(err)
diff --git a/vcr/credential/resolver_test.go b/vcr/credential/resolver_test.go
index 48dc7074d5..bec1b0ccb2 100644
--- a/vcr/credential/resolver_test.go
+++ b/vcr/credential/resolver_test.go
@@ -35,8 +35,8 @@ func TestFindValidator(t *testing.T) {
})
t.Run("validator and builder found for NutsOrganizationCredential", func(t *testing.T) {
- vc := validNutsOrganizationCredential()
- v := FindValidator(*vc)
+ vc := ValidNutsOrganizationCredential(t)
+ v := FindValidator(vc)
assert.NotNil(t, v)
})
diff --git a/vcr/credential/revocation_test.go b/vcr/credential/revocation_test.go
index b38ca71515..7a595e2ff0 100644
--- a/vcr/credential/revocation_test.go
+++ b/vcr/credential/revocation_test.go
@@ -26,14 +26,11 @@ import (
"time"
ssi "github.com/nuts-foundation/go-did"
- "github.com/nuts-foundation/go-did/vc"
"github.com/stretchr/testify/assert"
)
func TestBuildRevocation(t *testing.T) {
- target := vc.VerifiableCredential{}
- vcData, _ := os.ReadFile("../test/vc.json")
- json.Unmarshal(vcData, &target)
+ target := ValidNutsOrganizationCredential(t)
at := time.Now()
nowFunc = func() time.Time {
diff --git a/vcr/credential/test.go b/vcr/credential/test.go
index c6e0e6e2a0..363567b84e 100644
--- a/vcr/credential/test.go
+++ b/vcr/credential/test.go
@@ -21,7 +21,8 @@ package credential
import (
"encoding/json"
- "os"
+ "github.com/nuts-foundation/nuts-node/vcr/assets"
+ "testing"
"time"
ssi "github.com/nuts-foundation/go-did"
@@ -54,11 +55,17 @@ func ValidNutsAuthorizationCredential() *vc.VerifiableCredential {
}
}
-func validNutsOrganizationCredential() *vc.VerifiableCredential {
+func ValidNutsOrganizationCredential(t *testing.T) vc.VerifiableCredential {
inputVC := vc.VerifiableCredential{}
- vcJSON, _ := os.ReadFile("../test/vc.json")
- _ = json.Unmarshal(vcJSON, &inputVC)
- return &inputVC
+ vcJSON, err := assets.TestAssets.ReadFile("test_assets/vc.json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = json.Unmarshal(vcJSON, &inputVC)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return inputVC
}
func stringToURI(input string) ssi.URI {
diff --git a/vcr/credential/validator_test.go b/vcr/credential/validator_test.go
index 76093f1c04..5b4002b523 100644
--- a/vcr/credential/validator_test.go
+++ b/vcr/credential/validator_test.go
@@ -38,44 +38,44 @@ func TestNutsOrganizationCredentialValidator_Validate(t *testing.T) {
validator := nutsOrganizationCredentialValidator{}
t.Run("ok", func(t *testing.T) {
- v := validNutsOrganizationCredential()
+ v := ValidNutsOrganizationCredential(t)
- err := validator.Validate(*v)
+ err := validator.Validate(v)
assert.NoError(t, err)
})
t.Run("failed - missing custom type", func(t *testing.T) {
- v := validNutsOrganizationCredential()
+ v := ValidNutsOrganizationCredential(t)
v.Type = []ssi.URI{vc.VerifiableCredentialTypeV1URI()}
- err := validator.Validate(*v)
+ err := validator.Validate(v)
assert.EqualError(t, err, "validation failed: type 'NutsOrganizationCredential' is required")
})
t.Run("failed - missing credential subject", func(t *testing.T) {
- v := validNutsOrganizationCredential()
+ v := ValidNutsOrganizationCredential(t)
v.CredentialSubject = []interface{}{}
- err := validator.Validate(*v)
+ err := validator.Validate(v)
assert.EqualError(t, err, "validation failed: single CredentialSubject expected")
})
t.Run("failed - missing organization", func(t *testing.T) {
- v := validNutsOrganizationCredential()
+ v := ValidNutsOrganizationCredential(t)
var credentialSubject = make(map[string]interface{})
credentialSubject["id"] = vdr.TestDIDB.String()
v.CredentialSubject = []interface{}{credentialSubject}
- err := validator.Validate(*v)
+ err := validator.Validate(v)
assert.EqualError(t, err, "validation failed: 'credentialSubject.organization' is empty")
})
t.Run("failed - missing organization name", func(t *testing.T) {
- v := validNutsOrganizationCredential()
+ v := ValidNutsOrganizationCredential(t)
var credentialSubject = make(map[string]interface{})
credentialSubject["id"] = vdr.TestDIDB.String()
credentialSubject["organization"] = map[string]interface{}{
@@ -83,13 +83,13 @@ func TestNutsOrganizationCredentialValidator_Validate(t *testing.T) {
}
v.CredentialSubject = []interface{}{credentialSubject}
- err := validator.Validate(*v)
+ err := validator.Validate(v)
assert.EqualError(t, err, "validation failed: 'credentialSubject.name' is empty")
})
t.Run("failed - missing organization city", func(t *testing.T) {
- v := validNutsOrganizationCredential()
+ v := ValidNutsOrganizationCredential(t)
var credentialSubject = make(map[string]interface{})
credentialSubject["id"] = vdr.TestDIDB.String()
credentialSubject["organization"] = map[string]interface{}{
@@ -97,13 +97,13 @@ func TestNutsOrganizationCredentialValidator_Validate(t *testing.T) {
}
v.CredentialSubject = []interface{}{credentialSubject}
- err := validator.Validate(*v)
+ err := validator.Validate(v)
assert.EqualError(t, err, "validation failed: 'credentialSubject.city' is empty")
})
t.Run("failed - empty organization city", func(t *testing.T) {
- v := validNutsOrganizationCredential()
+ v := ValidNutsOrganizationCredential(t)
var credentialSubject = make(map[string]interface{})
credentialSubject["id"] = vdr.TestDIDB.String()
credentialSubject["organization"] = map[string]interface{}{
@@ -112,13 +112,13 @@ func TestNutsOrganizationCredentialValidator_Validate(t *testing.T) {
}
v.CredentialSubject = []interface{}{credentialSubject}
- err := validator.Validate(*v)
+ err := validator.Validate(v)
assert.EqualError(t, err, "validation failed: 'credentialSubject.city' is empty")
})
t.Run("failed - empty organization name", func(t *testing.T) {
- v := validNutsOrganizationCredential()
+ v := ValidNutsOrganizationCredential(t)
var credentialSubject = make(map[string]interface{})
credentialSubject["id"] = vdr.TestDIDB.String()
credentialSubject["organization"] = map[string]interface{}{
@@ -127,13 +127,13 @@ func TestNutsOrganizationCredentialValidator_Validate(t *testing.T) {
}
v.CredentialSubject = []interface{}{credentialSubject}
- err := validator.Validate(*v)
+ err := validator.Validate(v)
assert.EqualError(t, err, "validation failed: 'credentialSubject.name' is empty")
})
t.Run("failed - missing credentialSubject.ID", func(t *testing.T) {
- v := validNutsOrganizationCredential()
+ v := ValidNutsOrganizationCredential(t)
var credentialSubject = make(map[string]interface{})
credentialSubject["organization"] = map[string]interface{}{
"name": "Because we care B.V.",
@@ -141,13 +141,13 @@ func TestNutsOrganizationCredentialValidator_Validate(t *testing.T) {
}
v.CredentialSubject = []interface{}{credentialSubject}
- err := validator.Validate(*v)
+ err := validator.Validate(v)
assert.EqualError(t, err, "validation failed: 'credentialSubject.ID' is nil")
})
t.Run("failed - invalid credentialSubject.ID", func(t *testing.T) {
- v := validNutsOrganizationCredential()
+ v := ValidNutsOrganizationCredential(t)
var credentialSubject = make(map[string]interface{})
credentialSubject["id"] = "invalid"
credentialSubject["organization"] = map[string]interface{}{
@@ -156,27 +156,27 @@ func TestNutsOrganizationCredentialValidator_Validate(t *testing.T) {
}
v.CredentialSubject = []interface{}{credentialSubject}
- err := validator.Validate(*v)
+ err := validator.Validate(v)
assert.EqualError(t, err, "validation failed: invalid 'credentialSubject.id': invalid DID")
})
t.Run("failed - invalid ID", func(t *testing.T) {
- v := validNutsOrganizationCredential()
+ v := ValidNutsOrganizationCredential(t)
otherID := vdr.TestDIDB.URI()
v.ID = &otherID
- err := validator.Validate(*v)
+ err := validator.Validate(v)
assert.Error(t, err)
assert.EqualError(t, err, "validation failed: credential ID must start with issuer")
})
t.Run("failed - missing nuts context", func(t *testing.T) {
- v := validNutsOrganizationCredential()
+ v := ValidNutsOrganizationCredential(t)
v.Context = []ssi.URI{stringToURI("https://www.w3.org/2018/credentials/v1")}
- err := validator.Validate(*v)
+ err := validator.Validate(v)
assert.EqualError(t, err, "validation failed: context 'https://nuts.nl/credentials/v1' is required")
})
@@ -373,7 +373,7 @@ func TestNutsAuthorizationCredentialValidator_Validate(t *testing.T) {
func TestAllFieldsDefinedValidator(t *testing.T) {
validator := AllFieldsDefinedValidator{jsonld.NewTestJSONLDManager(t).DocumentLoader()}
t.Run("ok", func(t *testing.T) {
- inputVC := *validNutsOrganizationCredential()
+ inputVC := ValidNutsOrganizationCredential(t)
err := validator.Validate(inputVC)
@@ -387,7 +387,7 @@ func TestAllFieldsDefinedValidator(t *testing.T) {
"city": "EIbergen",
}
- inputVC := *validNutsOrganizationCredential()
+ inputVC := ValidNutsOrganizationCredential(t)
inputVC.CredentialSubject[0] = invalidCredentialSubject
err := validator.Validate(inputVC)
@@ -400,7 +400,7 @@ func TestDefaultCredentialValidator(t *testing.T) {
validator := defaultCredentialValidator{}
t.Run("ok - NutsOrganizationCredential", func(t *testing.T) {
- err := validator.Validate(*validNutsOrganizationCredential())
+ err := validator.Validate(ValidNutsOrganizationCredential(t))
assert.NoError(t, err)
})
@@ -420,37 +420,37 @@ func TestDefaultCredentialValidator(t *testing.T) {
})
t.Run("failed - missing ID", func(t *testing.T) {
- v := validNutsOrganizationCredential()
+ v := ValidNutsOrganizationCredential(t)
v.ID = nil
- err := validator.Validate(*v)
+ err := validator.Validate(v)
assert.EqualError(t, err, "validation failed: 'ID' is required")
})
t.Run("failed - missing proof", func(t *testing.T) {
- v := validNutsOrganizationCredential()
+ v := ValidNutsOrganizationCredential(t)
v.Proof = nil
- err := validator.Validate(*v)
+ err := validator.Validate(v)
assert.EqualError(t, err, "validation failed: 'proof' is required for JSON-LD credentials")
})
t.Run("failed - missing default context", func(t *testing.T) {
- v := validNutsOrganizationCredential()
+ v := ValidNutsOrganizationCredential(t)
v.Context = []ssi.URI{stringToURI(NutsV1Context)}
- err := validator.Validate(*v)
+ err := validator.Validate(v)
assert.EqualError(t, err, "validation failed: default context is required")
})
t.Run("failed - missing default type", func(t *testing.T) {
- v := validNutsOrganizationCredential()
+ v := ValidNutsOrganizationCredential(t)
v.Type = []ssi.URI{stringToURI(NutsOrganizationCredentialType)}
- err := validator.Validate(*v)
+ err := validator.Validate(v)
assert.EqualError(t, err, "validation failed: type 'VerifiableCredential' is required")
})
diff --git a/vcr/holder/openid.go b/vcr/holder/openid.go
index aa4c3ce927..ffabe98811 100644
--- a/vcr/holder/openid.go
+++ b/vcr/holder/openid.go
@@ -22,7 +22,8 @@ import (
"context"
"errors"
"fmt"
- "github.com/nuts-foundation/nuts-node/vdr/resolver"
+ "github.com/nuts-foundation/nuts-node/auth/oauth"
+ "github.com/nuts-foundation/nuts-node/openid4vc"
"net/http"
"time"
@@ -35,6 +36,7 @@ import (
"github.com/nuts-foundation/nuts-node/vcr/log"
"github.com/nuts-foundation/nuts-node/vcr/openid4vci"
vcrTypes "github.com/nuts-foundation/nuts-node/vcr/types"
+ "github.com/nuts-foundation/nuts-node/vdr/resolver"
)
// OpenIDHandler is the interface for handling issuer operations using OpenID4VCI.
@@ -94,7 +96,7 @@ func (h *openidHandler) HandleCredentialOffer(ctx context.Context, offer openid4
}
}
offeredCredential := offer.Credentials[0]
- if offeredCredential.Format != openid4vci.VerifiableCredentialJSONLDFormat {
+ if offeredCredential.Format != openid4vc.VerifiableCredentialJSONLDFormat {
return openid4vci.Error{
Err: fmt.Errorf("credential offer: unsupported format '%s'", offeredCredential.Format),
Code: openid4vci.UnsupportedCredentialType,
@@ -146,7 +148,7 @@ func (h *openidHandler) HandleCredentialOffer(ctx context.Context, offer openid4
}
}
- if accessTokenResponse.CNonce == "" {
+ if accessTokenResponse.CNonce == nil {
return openid4vci.Error{
Err: errors.New("c_nonce is missing"),
Code: openid4vci.InvalidToken,
@@ -192,7 +194,7 @@ func getPreAuthorizedCodeFromOffer(offer openid4vci.CredentialOffer) string {
return preAuthorizedCode
}
-func (h *openidHandler) retrieveCredential(ctx context.Context, issuerClient openid4vci.IssuerAPIClient, offer *openid4vci.CredentialDefinition, tokenResponse *openid4vci.TokenResponse) (*vc.VerifiableCredential, error) {
+func (h *openidHandler) retrieveCredential(ctx context.Context, issuerClient openid4vci.IssuerAPIClient, offer *openid4vci.CredentialDefinition, tokenResponse *oauth.TokenResponse) (*vc.VerifiableCredential, error) {
keyID, _, err := h.resolver.ResolveKey(h.did, nil, resolver.NutsSigningKeyType)
headers := map[string]interface{}{
"typ": openid4vci.JWTTypeOpenID4VCIProof, // MUST be openid4vci-proof+jwt, which explicitly types the proof JWT as recommended in Section 3.11 of [RFC8725].
@@ -211,7 +213,7 @@ func (h *openidHandler) retrieveCredential(ctx context.Context, issuerClient ope
credentialRequest := openid4vci.CredentialRequest{
CredentialDefinition: offer,
- Format: openid4vci.VerifiableCredentialJSONLDFormat,
+ Format: openid4vc.VerifiableCredentialJSONLDFormat,
Proof: &openid4vci.CredentialRequestProof{
Jwt: proof,
ProofType: "jwt",
diff --git a/vcr/holder/openid_test.go b/vcr/holder/openid_test.go
index a7bca1967a..2856ed2982 100644
--- a/vcr/holder/openid_test.go
+++ b/vcr/holder/openid_test.go
@@ -25,8 +25,10 @@ import (
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/go-did/vc"
"github.com/nuts-foundation/nuts-node/audit"
+ "github.com/nuts-foundation/nuts-node/auth/oauth"
"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/crypto"
+ "github.com/nuts-foundation/nuts-node/openid4vc"
"github.com/nuts-foundation/nuts-node/vcr/openid4vci"
"github.com/nuts-foundation/nuts-node/vcr/types"
"github.com/nuts-foundation/nuts-node/vdr/resolver"
@@ -71,17 +73,16 @@ func Test_wallet_HandleCredentialOffer(t *testing.T) {
CredentialIssuer: issuerDID.String(),
CredentialEndpoint: "credential-endpoint",
}
+ nonce := "nonsens"
t.Run("ok", func(t *testing.T) {
ctrl := gomock.NewController(t)
- nonce := "nonsens"
issuerAPIClient := openid4vci.NewMockIssuerAPIClient(ctrl)
issuerAPIClient.EXPECT().Metadata().Return(metadata)
issuerAPIClient.EXPECT().RequestAccessToken("urn:ietf:params:oauth:grant-type:pre-authorized_code", map[string]string{
"pre-authorized_code": "code",
- }).Return(&openid4vci.TokenResponse{
+ }).Return(&oauth.TokenResponse{
AccessToken: "access-token",
- CNonce: nonce,
- ExpiresIn: 0,
+ CNonce: &nonce,
TokenType: "bearer",
}, nil)
issuerAPIClient.EXPECT().RequestCredential(gomock.Any(), gomock.Any(), "access-token").
@@ -95,7 +96,7 @@ func Test_wallet_HandleCredentialOffer(t *testing.T) {
jwtSigner.EXPECT().SignJWT(gomock.Any(), map[string]interface{}{
"aud": issuerDID.String(),
"iat": int64(1735689600),
- "nonce": nonce,
+ "nonce": &nonce,
}, gomock.Any(), "key-id").Return("signed-jwt", nil)
keyResolver := resolver.NewMockKeyResolver(ctrl)
keyResolver.EXPECT().ResolveKey(holderDID, nil, resolver.NutsSigningKeyType).Return(ssi.MustParseURI("key-id"), nil, nil)
@@ -176,7 +177,7 @@ func Test_wallet_HandleCredentialOffer(t *testing.T) {
t.Run("error - empty access token", func(t *testing.T) {
ctrl := gomock.NewController(t)
issuerAPIClient := openid4vci.NewMockIssuerAPIClient(ctrl)
- issuerAPIClient.EXPECT().RequestAccessToken(gomock.Any(), gomock.Any()).Return(&openid4vci.TokenResponse{}, nil)
+ issuerAPIClient.EXPECT().RequestAccessToken(gomock.Any(), gomock.Any()).Return(&oauth.TokenResponse{}, nil)
w := NewOpenIDHandler(holderDID, "https://holder.example.com", &http.Client{}, nil, nil, nil).(*openidHandler)
w.issuerClientCreator = func(_ context.Context, httpClient core.HTTPRequestDoer, credentialIssuerIdentifier string) (openid4vci.IssuerAPIClient, error) {
@@ -190,7 +191,7 @@ func Test_wallet_HandleCredentialOffer(t *testing.T) {
t.Run("error - empty c_nonce", func(t *testing.T) {
ctrl := gomock.NewController(t)
issuerAPIClient := openid4vci.NewMockIssuerAPIClient(ctrl)
- issuerAPIClient.EXPECT().RequestAccessToken(gomock.Any(), gomock.Any()).Return(&openid4vci.TokenResponse{AccessToken: "foo"}, nil)
+ issuerAPIClient.EXPECT().RequestAccessToken(gomock.Any(), gomock.Any()).Return(&oauth.TokenResponse{AccessToken: "foo"}, nil)
w := NewOpenIDHandler(holderDID, "https://holder.example.com", &http.Client{}, nil, nil, nil).(*openidHandler)
w.issuerClientCreator = func(_ context.Context, httpClient core.HTTPRequestDoer, credentialIssuerIdentifier string) (openid4vci.IssuerAPIClient, error) {
@@ -230,7 +231,7 @@ func Test_wallet_HandleCredentialOffer(t *testing.T) {
ctrl := gomock.NewController(t)
issuerAPIClient := openid4vci.NewMockIssuerAPIClient(ctrl)
issuerAPIClient.EXPECT().Metadata().Return(metadata)
- issuerAPIClient.EXPECT().RequestAccessToken(gomock.Any(), gomock.Any()).Return(&openid4vci.TokenResponse{AccessToken: "access-token", CNonce: "c_nonce"}, nil)
+ issuerAPIClient.EXPECT().RequestAccessToken(gomock.Any(), gomock.Any()).Return(&oauth.TokenResponse{AccessToken: "access-token", CNonce: &nonce}, nil)
issuerAPIClient.EXPECT().RequestCredential(gomock.Any(), gomock.Any(), gomock.Any()).Return(&vc.VerifiableCredential{
Context: offer.CredentialDefinition.Context,
Type: []ssi.URI{ssi.MustParseURI("VerifiableCredential")},
@@ -274,7 +275,7 @@ func Test_wallet_HandleCredentialOffer(t *testing.T) {
// offeredCredential returns a structure that can be used as CredentialOffer.Credentials,
func offeredCredential() []openid4vci.OfferedCredential {
return []openid4vci.OfferedCredential{{
- Format: openid4vci.VerifiableCredentialJSONLDFormat,
+ Format: openid4vc.VerifiableCredentialJSONLDFormat,
CredentialDefinition: &openid4vci.CredentialDefinition{
Context: []ssi.URI{
ssi.MustParseURI("https://www.w3.org/2018/credentials/v1"),
diff --git a/vcr/issuer/openid.go b/vcr/issuer/openid.go
index cdaa200475..889460d628 100644
--- a/vcr/issuer/openid.go
+++ b/vcr/issuer/openid.go
@@ -21,8 +21,6 @@ package issuer
import (
"context"
crypt "crypto"
- "crypto/rand"
- "encoding/base64"
"encoding/json"
"errors"
"fmt"
@@ -34,6 +32,7 @@ import (
"github.com/nuts-foundation/nuts-node/audit"
"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/crypto"
+ "github.com/nuts-foundation/nuts-node/openid4vc"
"github.com/nuts-foundation/nuts-node/storage"
"github.com/nuts-foundation/nuts-node/vcr/issuer/assets"
"github.com/nuts-foundation/nuts-node/vcr/log"
@@ -77,9 +76,6 @@ const preAuthCodeRefType = "preauthcode"
const accessTokenRefType = "accesstoken"
const cNonceRefType = "c_nonce"
-// openidSecretSizeBits is the size of the generated random secrets (access tokens, pre-authorized codes) in bits.
-const openidSecretSizeBits = 128
-
// OpenIDHandler defines the interface for handling OpenID4VCI issuer operations.
type OpenIDHandler interface {
// ProviderMetadata returns the OpenID Connect provider metadata.
@@ -164,12 +160,12 @@ func (i *openidHandler) HandleAccessTokenRequest(ctx context.Context, preAuthori
StatusCode: http.StatusBadRequest,
}
}
- accessToken := generateCode()
+ accessToken := crypto.GenerateNonce()
err = i.store.StoreReference(ctx, flow.ID, accessTokenRefType, accessToken)
if err != nil {
return "", "", err
}
- cNonce := generateCode()
+ cNonce := crypto.GenerateNonce()
err = i.store.StoreReference(ctx, flow.ID, cNonceRefType, cNonce)
if err != nil {
return "", "", err
@@ -188,7 +184,7 @@ func (i *openidHandler) HandleAccessTokenRequest(ctx context.Context, preAuthori
}
func (i *openidHandler) OfferCredential(ctx context.Context, credential vc.VerifiableCredential, walletIdentifier string) error {
- preAuthorizedCode := generateCode()
+ preAuthorizedCode := crypto.GenerateNonce()
walletMetadataURL := core.JoinURLPaths(walletIdentifier, openid4vci.WalletMetadataWellKnownPath)
log.Logger().
WithField(core.LogFieldCredentialID, credential.ID).
@@ -212,7 +208,7 @@ func (i *openidHandler) OfferCredential(ctx context.Context, credential vc.Verif
}
func (i *openidHandler) HandleCredentialRequest(ctx context.Context, request openid4vci.CredentialRequest, accessToken string) (*vc.VerifiableCredential, error) {
- if request.Format != openid4vci.VerifiableCredentialJSONLDFormat {
+ if request.Format != openid4vc.VerifiableCredentialJSONLDFormat {
return nil, openid4vci.Error{
Err: fmt.Errorf("credential request: unsupported format '%s'", request.Format),
Code: openid4vci.UnsupportedCredentialType,
@@ -284,7 +280,7 @@ func (i *openidHandler) validateProof(ctx context.Context, flow *Flow, request o
// augment invalid_proof errors according to ยง7.3.2 of openid4vci spec
generateProofError := func(err openid4vci.Error) error {
- cnonce := generateCode()
+ cnonce := crypto.GenerateNonce()
if err := i.store.StoreReference(ctx, flow.ID, cNonceRefType, cnonce); err != nil {
return err
}
@@ -413,7 +409,7 @@ func (i *openidHandler) createOffer(ctx context.Context, credential vc.Verifiabl
offer := openid4vci.CredentialOffer{
CredentialIssuer: i.issuerIdentifierURL,
Credentials: []openid4vci.OfferedCredential{{
- Format: openid4vci.VerifiableCredentialJSONLDFormat,
+ Format: openid4vc.VerifiableCredentialJSONLDFormat,
CredentialDefinition: &openid4vci.CredentialDefinition{
Context: credential.Context,
Type: credential.Type,
@@ -492,15 +488,6 @@ func (i *openidHandler) loadCredentialDefinitions() error {
return err
}
-func generateCode() string {
- buf := make([]byte, openidSecretSizeBits/8)
- _, err := rand.Read(buf)
- if err != nil {
- panic(err)
- }
- return base64.URLEncoding.EncodeToString(buf)
-}
-
func deepcopy(src []map[string]interface{}) []map[string]interface{} {
dst := make([]map[string]interface{}, len(src))
for i := range src {
diff --git a/vcr/issuer/openid_test.go b/vcr/issuer/openid_test.go
index 281eeaaa0f..449de9eda6 100644
--- a/vcr/issuer/openid_test.go
+++ b/vcr/issuer/openid_test.go
@@ -28,6 +28,7 @@ import (
"github.com/nuts-foundation/nuts-node/audit"
"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/crypto"
+ "github.com/nuts-foundation/nuts-node/openid4vc"
"github.com/nuts-foundation/nuts-node/storage"
"github.com/nuts-foundation/nuts-node/vcr/openid4vci"
"github.com/nuts-foundation/nuts-node/vdr/resolver"
@@ -143,7 +144,7 @@ func Test_memoryIssuer_HandleCredentialRequest(t *testing.T) {
proof, err := keyStore.SignJWT(ctx, claims, headers, headers["kid"])
require.NoError(t, err)
return openid4vci.CredentialRequest{
- Format: openid4vci.VerifiableCredentialJSONLDFormat,
+ Format: openid4vc.VerifiableCredentialJSONLDFormat,
CredentialDefinition: &openid4vci.CredentialDefinition{
Context: []ssi.URI{
ssi.MustParseURI("https://www.w3.org/2018/credentials/v1"),
diff --git a/vcr/openid4vci/issuer_client.go b/vcr/openid4vci/issuer_client.go
index 7a20812bae..abac02d343 100644
--- a/vcr/openid4vci/issuer_client.go
+++ b/vcr/openid4vci/issuer_client.go
@@ -25,6 +25,7 @@ import (
"errors"
"fmt"
"github.com/nuts-foundation/go-did/vc"
+ "github.com/nuts-foundation/nuts-node/auth/oauth"
"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/vcr/log"
"io"
@@ -192,7 +193,7 @@ func httpDo(httpClient core.HTTPRequestDoer, httpRequest *http.Request, result i
// OAuth2Client defines a generic OAuth2 client.
type OAuth2Client interface {
// RequestAccessToken requests an access token from the Authorization Server.
- RequestAccessToken(grantType string, params map[string]string) (*TokenResponse, error)
+ RequestAccessToken(grantType string, params map[string]string) (*oauth.TokenResponse, error)
}
var _ OAuth2Client = &httpOAuth2Client{}
@@ -202,7 +203,7 @@ type httpOAuth2Client struct {
httpClient core.HTTPRequestDoer
}
-func (c httpOAuth2Client) RequestAccessToken(grantType string, params map[string]string) (*TokenResponse, error) {
+func (c httpOAuth2Client) RequestAccessToken(grantType string, params map[string]string) (*oauth.TokenResponse, error) {
values := url.Values{}
values.Add("grant_type", grantType)
for key, value := range params {
@@ -210,7 +211,7 @@ func (c httpOAuth2Client) RequestAccessToken(grantType string, params map[string
}
httpRequest, _ := http.NewRequestWithContext(context.Background(), "POST", c.metadata.TokenEndpoint, strings.NewReader(values.Encode()))
httpRequest.Header.Add("Content-Type", "application/x-www-form-urlencoded")
- var accessTokenResponse TokenResponse
+ var accessTokenResponse oauth.TokenResponse
err := httpDo(c.httpClient, httpRequest, &accessTokenResponse)
if err != nil {
return nil, fmt.Errorf("request access token error: %w", err)
diff --git a/vcr/openid4vci/issuer_client_mock.go b/vcr/openid4vci/issuer_client_mock.go
index 13b000e602..9da6b18275 100644
--- a/vcr/openid4vci/issuer_client_mock.go
+++ b/vcr/openid4vci/issuer_client_mock.go
@@ -13,6 +13,7 @@ import (
reflect "reflect"
vc "github.com/nuts-foundation/go-did/vc"
+ oauth "github.com/nuts-foundation/nuts-node/auth/oauth"
gomock "go.uber.org/mock/gomock"
)
@@ -54,10 +55,10 @@ func (mr *MockIssuerAPIClientMockRecorder) Metadata() *gomock.Call {
}
// RequestAccessToken mocks base method.
-func (m *MockIssuerAPIClient) RequestAccessToken(grantType string, params map[string]string) (*TokenResponse, error) {
+func (m *MockIssuerAPIClient) RequestAccessToken(grantType string, params map[string]string) (*oauth.TokenResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RequestAccessToken", grantType, params)
- ret0, _ := ret[0].(*TokenResponse)
+ ret0, _ := ret[0].(*oauth.TokenResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -107,10 +108,10 @@ func (m *MockOAuth2Client) EXPECT() *MockOAuth2ClientMockRecorder {
}
// RequestAccessToken mocks base method.
-func (m *MockOAuth2Client) RequestAccessToken(grantType string, params map[string]string) (*TokenResponse, error) {
+func (m *MockOAuth2Client) RequestAccessToken(grantType string, params map[string]string) (*oauth.TokenResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RequestAccessToken", grantType, params)
- ret0, _ := ret[0].(*TokenResponse)
+ ret0, _ := ret[0].(*oauth.TokenResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
diff --git a/vcr/openid4vci/issuer_client_test.go b/vcr/openid4vci/issuer_client_test.go
index 2e7f48491e..2c0fbcad20 100644
--- a/vcr/openid4vci/issuer_client_test.go
+++ b/vcr/openid4vci/issuer_client_test.go
@@ -20,6 +20,7 @@ package openid4vci
import (
"context"
+ "github.com/nuts-foundation/nuts-node/openid4vc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"net/http"
@@ -89,7 +90,7 @@ func Test_httpIssuerClient_RequestCredential(t *testing.T) {
httpClient := &http.Client{}
credentialRequest := CredentialRequest{
CredentialDefinition: &CredentialDefinition{},
- Format: VerifiableCredentialJSONLDFormat,
+ Format: openid4vc.VerifiableCredentialJSONLDFormat,
}
t.Run("ok", func(t *testing.T) {
setup := setupClientTest(t)
diff --git a/vcr/openid4vci/test.go b/vcr/openid4vci/test.go
index eb5c88dc70..8435c1ce7e 100644
--- a/vcr/openid4vci/test.go
+++ b/vcr/openid4vci/test.go
@@ -22,6 +22,8 @@ import (
"context"
"encoding/json"
"fmt"
+ "github.com/nuts-foundation/nuts-node/auth/oauth"
+ "github.com/nuts-foundation/nuts-node/openid4vc"
"github.com/nuts-foundation/nuts-node/test"
"net/http"
"testing"
@@ -34,7 +36,7 @@ func setupClientTest(t *testing.T) *oidcClientTestContext {
providerMetadata := new(ProviderMetadata)
walletMetadata := new(OAuth2ClientMetadata)
credentialResponse := CredentialResponse{
- Format: VerifiableCredentialJSONLDFormat,
+ Format: openid4vc.VerifiableCredentialJSONLDFormat,
Credential: &map[string]interface{}{
"@context": []string{"https://www.w3.org/2018/credentials/v1"},
"type": []string{"VerifiableCredential"},
@@ -51,7 +53,7 @@ func setupClientTest(t *testing.T) *oidcClientTestContext {
clientTest.issuerMetadataHandler = clientTest.httpGetHandler(issuerMetadata)
clientTest.providerMetadataHandler = clientTest.httpGetHandler(providerMetadata)
clientTest.credentialHandler = clientTest.httpPostHandler(credentialResponse)
- clientTest.tokenHandler = clientTest.httpPostHandler(TokenResponse{AccessToken: "secret"})
+ clientTest.tokenHandler = clientTest.httpPostHandler(oauth.TokenResponse{AccessToken: "secret"})
clientTest.walletMetadataHandler = clientTest.httpGetHandler(walletMetadata)
clientTest.credentialOfferHandler = clientTest.httpGetHandler(CredentialOfferResponse{CredentialOfferStatusReceived})
diff --git a/vcr/openid4vci/types.go b/vcr/openid4vci/types.go
index 37ca8544bc..e5d030b005 100644
--- a/vcr/openid4vci/types.go
+++ b/vcr/openid4vci/types.go
@@ -41,9 +41,6 @@ const ProviderMetadataWellKnownPath = "/.well-known/oauth-authorization-server"
// Specified by https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-issuer-metadata-
const CredentialIssuerMetadataWellKnownPath = "/.well-known/openid-credential-issuer"
-// VerifiableCredentialJSONLDFormat defines the JSON-LD format identifier for Verifiable Credentials.
-const VerifiableCredentialJSONLDFormat = "ldp_vc"
-
// JWTTypeOpenID4VCIProof defines the OpenID4VCI JWT-subtype (used as typ claim in the JWT).
const JWTTypeOpenID4VCIProof = "openid4vci-proof+jwt"
@@ -150,25 +147,6 @@ type CredentialResponse struct {
CNonce *string `json:"c_nonce,omitempty"`
}
-// TokenResponse defines the response for OAuth2 access token requests, extended with OpenID4VCI parameters.
-// Specified by https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-successful-token-response
-type TokenResponse struct {
- // AccessToken defines the access token issued by the authorization server.
- AccessToken string `json:"access_token"`
-
- // CNonce defines the JSON string containing a nonce to be used to create a proof of possession of key material when requesting a Credential.
- // When received, the WalletAPIClient MUST use this nonce value for its subsequent credential requests until the Credential Issuer provides a fresh nonce.
- // Although optional in the spec, we use a concrete value since we always fill it.
- CNonce string `json:"c_nonce,omitempty"`
-
- // ExpiresIn defines the lifetime in seconds of the access token.
- // Although optional in the spec, we use a concrete value since we always fill it.
- ExpiresIn int `json:"expires_in,omitempty"`
-
- // TokenType defines the type of the token issued as described in [RFC6749].
- TokenType string `json:"token_type"`
-}
-
// Config holds the config for the OpenID4VCI credential issuer and wallet
type Config struct {
// DefinitionsDIR defines the directory where the additional credential definitions are stored
diff --git a/vcr/pe/presentation_definition_test.go b/vcr/pe/presentation_definition_test.go
index b935bf2c15..32ecd602d5 100644
--- a/vcr/pe/presentation_definition_test.go
+++ b/vcr/pe/presentation_definition_test.go
@@ -20,13 +20,14 @@ package pe
import (
"encoding/json"
+ "testing"
+
ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/go-did/vc"
+ "github.com/nuts-foundation/nuts-node/vcr/credential"
"github.com/nuts-foundation/nuts-node/vcr/pe/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "os"
- "testing"
)
const testPresentationDefinition = `
@@ -100,13 +101,11 @@ func TestMatch(t *testing.T) {
vc2 := vc.VerifiableCredential{ID: &id2}
vc3 := vc.VerifiableCredential{ID: &id3}
vc4 := vc.VerifiableCredential{ID: &id4}
+ verifiableCredential := credential.ValidNutsOrganizationCredential(t)
t.Run("Basic", func(t *testing.T) {
presentationDefinition := PresentationDefinition{}
_ = json.Unmarshal([]byte(testPresentationDefinition), &presentationDefinition)
- verifiableCredential := vc.VerifiableCredential{}
- vcJSON, _ := os.ReadFile("../test/vc.json")
- _ = json.Unmarshal(vcJSON, &verifiableCredential)
t.Run("Happy flow", func(t *testing.T) {
vcs, mappingObjects, err := presentationDefinition.Match([]vc.VerifiableCredential{verifiableCredential})
@@ -271,9 +270,7 @@ func TestMatch(t *testing.T) {
}
func Test_matchFormat(t *testing.T) {
- verifiableCredential := vc.VerifiableCredential{}
- vcJSON, _ := os.ReadFile("../test/vc.json")
- _ = json.Unmarshal(vcJSON, &verifiableCredential)
+ verifiableCredential := credential.ValidNutsOrganizationCredential(t)
t.Run("no format", func(t *testing.T) {
match := matchFormat(nil, vc.VerifiableCredential{})
diff --git a/vcr/pe/presentation_submission.go b/vcr/pe/presentation_submission.go
index 5771e4443f..fa858d199a 100644
--- a/vcr/pe/presentation_submission.go
+++ b/vcr/pe/presentation_submission.go
@@ -75,9 +75,27 @@ type SignInstruction struct {
inputDescriptorMappingObjects []InputDescriptorMappingObject
}
+// Empty returns true if there are no VCs in the SignInstruction.
+func (signInstruction SignInstruction) Empty() bool {
+ return len(signInstruction.VerifiableCredentials) == 0
+}
+
+// SignInstructions is a list of SignInstruction.
+type SignInstructions []SignInstruction
+
+// Empty returns true if all SignInstructions are empty.
+func (signInstructions SignInstructions) Empty() bool {
+ for _, signInstruction := range []SignInstruction(signInstructions) {
+ if !signInstruction.Empty() {
+ return false
+ }
+ }
+ return true
+}
+
// Build creates a PresentationSubmission from the added wallets.
// The VP format is determined by the given format.
-func (b *PresentationSubmissionBuilder) Build(format string) (PresentationSubmission, []SignInstruction, error) {
+func (b *PresentationSubmissionBuilder) Build(format string) (PresentationSubmission, SignInstructions, error) {
presentationSubmission := PresentationSubmission{
Id: uuid.New().String(),
DefinitionId: b.presentationDefinition.Id,
diff --git a/vcr/store_test.go b/vcr/store_test.go
index 85585b80fc..f9fd84e429 100644
--- a/vcr/store_test.go
+++ b/vcr/store_test.go
@@ -26,6 +26,7 @@ import (
"encoding/json"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/nuts-node/crypto/storage/spi"
+ "github.com/nuts-foundation/nuts-node/vcr/credential"
"github.com/nuts-foundation/nuts-node/vdr/resolver"
"github.com/stretchr/testify/require"
"os"
@@ -39,9 +40,7 @@ import (
func TestVcr_StoreCredential(t *testing.T) {
// load VC
- target := vc.VerifiableCredential{}
- vcJSON, _ := os.ReadFile("test/vc.json")
- json.Unmarshal(vcJSON, &target)
+ target := credential.ValidNutsOrganizationCredential(t)
holderDID := did.MustParseDID(target.CredentialSubject[0].(map[string]interface{})["id"].(string))
// load pub key
@@ -134,9 +133,7 @@ func TestVcr_StoreCredential(t *testing.T) {
func TestStore_writeCredential(t *testing.T) {
// load VC
- target := vc.VerifiableCredential{}
- vcJSON, _ := os.ReadFile("test/vc.json")
- json.Unmarshal(vcJSON, &target)
+ target := credential.ValidNutsOrganizationCredential(t)
t.Run("ok - stored in JSON-LD collection", func(t *testing.T) {
ctx := newMockContext(t)
diff --git a/vcr/test/openid4vci_integration_test.go b/vcr/test/openid4vci_integration_test.go
index fd9a0f943c..0aed84a117 100644
--- a/vcr/test/openid4vci_integration_test.go
+++ b/vcr/test/openid4vci_integration_test.go
@@ -24,6 +24,7 @@ import (
"github.com/nuts-foundation/nuts-node/core"
httpModule "github.com/nuts-foundation/nuts-node/http"
"github.com/nuts-foundation/nuts-node/network/log"
+ "github.com/nuts-foundation/nuts-node/openid4vc"
"github.com/nuts-foundation/nuts-node/vcr/issuer"
"github.com/nuts-foundation/nuts-node/vcr/openid4vci"
"github.com/nuts-foundation/nuts-node/vdr"
@@ -229,7 +230,7 @@ func TestOpenID4VCIErrorResponses(t *testing.T) {
require.NoError(t, err)
requestBody, _ := json.Marshal(openid4vci.CredentialRequest{
- Format: openid4vci.VerifiableCredentialJSONLDFormat,
+ Format: openid4vc.VerifiableCredentialJSONLDFormat,
})
t.Run("error from API layer (missing access token)", func(t *testing.T) {
diff --git a/vcr/vcr.go b/vcr/vcr.go
index a5a1a8f7b7..acd60de598 100644
--- a/vcr/vcr.go
+++ b/vcr/vcr.go
@@ -256,19 +256,9 @@ func (c *vcr) Configure(config core.ServerConfig) error {
// This is because the credential is requested by the wallet synchronously during the offer handling,
// meaning while the issuer allocated an HTTP connection the wallet will try to allocate one as well.
// This moved back to 1 http.Client when the credential is requested asynchronously.
- // Should be fixed as part of https://github.com/nuts-foundation/nuts-node/issues/2039
- issuerTransport := http.DefaultTransport.(*http.Transport).Clone()
- issuerTransport.TLSClientConfig = tlsConfig
- c.issuerHttpClient = core.NewStrictHTTPClient(config.Strictmode, &http.Client{
- Timeout: c.config.OpenID4VCI.Timeout,
- Transport: issuerTransport,
- })
- walletTransport := http.DefaultTransport.(*http.Transport).Clone()
- walletTransport.TLSClientConfig = tlsConfig
- c.walletHttpClient = core.NewStrictHTTPClient(config.Strictmode, &http.Client{
- Timeout: c.config.OpenID4VCI.Timeout,
- Transport: walletTransport,
- })
+ // Should be fixed as part of https://github.com/nuts-foundation/nuts-node/issues/2039 (also fix core.NewStrictHTTPClient)
+ c.issuerHttpClient = core.NewStrictHTTPClient(config.Strictmode, c.config.OpenID4VCI.Timeout, tlsConfig)
+ c.walletHttpClient = core.NewStrictHTTPClient(config.Strictmode, c.config.OpenID4VCI.Timeout, tlsConfig)
c.openidSessionStore = c.storageClient.GetSessionDatabase()
}
c.issuer = issuer.NewIssuer(c.issuerStore, c, networkPublisher, openidHandlerFn, didResolver, c.keyStore, c.jsonldManager, c.trustConfig)
diff --git a/vcr/vcr_test.go b/vcr/vcr_test.go
index ccb85482d4..f9389d9055 100644
--- a/vcr/vcr_test.go
+++ b/vcr/vcr_test.go
@@ -30,6 +30,7 @@ import (
"github.com/nuts-foundation/nuts-node/audit"
"github.com/nuts-foundation/nuts-node/pki"
"github.com/nuts-foundation/nuts-node/storage"
+ "github.com/nuts-foundation/nuts-node/vcr/credential"
"github.com/nuts-foundation/nuts-node/vcr/holder"
"github.com/nuts-foundation/nuts-node/vcr/openid4vci"
"github.com/nuts-foundation/nuts-node/vdr"
@@ -80,9 +81,7 @@ func TestVCR_Configure(t *testing.T) {
})
t.Run("strictmode passed to client APIs", func(t *testing.T) {
// load test VC
- testVC := vc.VerifiableCredential{}
- vcJSON, _ := os.ReadFile("test/vc.json")
- _ = json.Unmarshal(vcJSON, &testVC)
+ testVC := credential.ValidNutsOrganizationCredential(t)
issuerDID := did.MustParseDID(testVC.Issuer.String())
testDirectory := io.TestDirectory(t)
ctrl := gomock.NewController(t)
diff --git a/vcr/verifier/verifier_test.go b/vcr/verifier/verifier_test.go
index 9bbffea192..57746924dc 100644
--- a/vcr/verifier/verifier_test.go
+++ b/vcr/verifier/verifier_test.go
@@ -54,9 +54,7 @@ import (
)
func testCredential(t *testing.T) vc.VerifiableCredential {
- subject := vc.VerifiableCredential{}
- vcJSON, _ := os.ReadFile("../test/vc.json")
- require.NoError(t, json.Unmarshal(vcJSON, &subject))
+ subject := credential.ValidNutsOrganizationCredential(t)
return subject
}
diff --git a/vdr/didweb/test.go b/vdr/didweb/test.go
new file mode 100644
index 0000000000..51e4aa14be
--- /dev/null
+++ b/vdr/didweb/test.go
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2023 Nuts community
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package didweb
+
+import (
+ "github.com/nuts-foundation/go-did/did"
+ "github.com/stretchr/testify/require"
+ "net/url"
+ "strings"
+ "testing"
+)
+
+func ServerURLToDIDWeb(t *testing.T, stringUrl string) did.DID {
+ stringUrl = strings.ReplaceAll(stringUrl, "127.0.0.1", "localhost")
+ asURL, err := url.Parse(stringUrl)
+ require.NoError(t, err)
+ testDID, err := URLToDID(*asURL)
+ require.NoError(t, err)
+ return *testDID
+}