diff --git a/auth/api/iam/api.go b/auth/api/iam/api.go index ce87ed81d9..8c9a9b8db5 100644 --- a/auth/api/iam/api.go +++ b/auth/api/iam/api.go @@ -28,13 +28,13 @@ import ( "github.com/nuts-foundation/nuts-node/auth" "github.com/nuts-foundation/nuts-node/auth/log" "github.com/nuts-foundation/nuts-node/core" + "github.com/nuts-foundation/nuts-node/storage" "github.com/nuts-foundation/nuts-node/vcr" "github.com/nuts-foundation/nuts-node/vdr" "github.com/nuts-foundation/nuts-node/vdr/resolver" "html/template" "net/http" "strings" - "sync" ) var _ core.Routable = &Wrapper{} @@ -49,25 +49,25 @@ var assets embed.FS // Wrapper handles OAuth2 flows. type Wrapper struct { - vcr vcr.VCR - vdr vdr.VDR - auth auth.AuthenticationServices - sessions *SessionManager - templates *template.Template + vcr vcr.VCR + vdr vdr.VDR + auth auth.AuthenticationServices + templates *template.Template + storageEngine storage.Engine } -func New(authInstance auth.AuthenticationServices, vcrInstance vcr.VCR, vdrInstance vdr.VDR) *Wrapper { +func New(authInstance auth.AuthenticationServices, vcrInstance vcr.VCR, vdrInstance vdr.VDR, storageEngine storage.Engine) *Wrapper { templates := template.New("oauth2 templates") _, err := templates.ParseFS(assets, "assets/*.html") if err != nil { panic(err) } return &Wrapper{ - sessions: &SessionManager{sessions: new(sync.Map)}, - auth: authInstance, - vcr: vcrInstance, - vdr: vdrInstance, - templates: templates, + storageEngine: storageEngine, + auth: authInstance, + vcr: vcrInstance, + vdr: vdrInstance, + templates: templates, } } diff --git a/auth/api/iam/authorized_code.go b/auth/api/iam/authorized_code.go deleted file mode 100644 index 2e4cc2521b..0000000000 --- a/auth/api/iam/authorized_code.go +++ /dev/null @@ -1,115 +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 ( - "bytes" - "crypto/rand" - "encoding/base64" - "errors" - "fmt" - "github.com/labstack/echo/v4" - "github.com/nuts-foundation/nuts-node/core" - "html/template" - "net/http" - "net/url" -) - -func newAuthorizedCodeFlow(sessions *SessionManager) *authorizedCodeFlow { - authzTemplate, _ := template.ParseFS(assets, "assets/authz_en.html") - return &authorizedCodeFlow{ - sessions: sessions, - authzTemplate: authzTemplate, - } -} - -// authorizedCodeFlow implements the grant type as specified by https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3. -type authorizedCodeFlow struct { - sessions *SessionManager - authzTemplate *template.Template -} - -func (a authorizedCodeFlow) Routes(router core.EchoRouter) { - router.Add(http.MethodPost, "/public/oauth2/:did/authz_consent", a.handleAuthConsent) -} - -func (a authorizedCodeFlow) handleAuthzRequest(params map[string]string, session *Session) (*authzResponse, error) { - // This authz request handling is just for demonstration purposes. - sessionId := a.sessions.Create(*session) - - // Render HTML - buf := new(bytes.Buffer) - // TODO: Support multiple languages - err := a.authzTemplate.Execute(buf, struct { - SessionID string - Session - }{ - SessionID: sessionId, - Session: *session, - }) - if err != nil { - return nil, fmt.Errorf("unable to render authorization page: %w", err) - } - return &authzResponse{ - html: buf.Bytes(), - }, nil -} - -// handleAuthConsent handles the authorization consent form submission. -func (a authorizedCodeFlow) handleAuthConsent(c echo.Context) error { - var session *Session - if sessionID := c.Param("sessionID"); sessionID != "" { - session = a.sessions.Get(sessionID) - } - if session == nil { - return errors.New("invalid session") - } - - redirectURI, _ := url.Parse(session.RedirectURI) // Validated on session creation, can't fail - query := redirectURI.Query() - query.Add("code", generateCode()) - redirectURI.RawQuery = query.Encode() - - return c.Redirect(http.StatusFound, redirectURI.String()) -} - -func (a authorizedCodeFlow) validateCode(params map[string]string) (string, error) { - code, ok := params["code"] - invalidCodeError := OAuth2Error{ - Code: InvalidRequest, - Description: "missing or invalid code parameter", - } - if !ok { - return "", invalidCodeError - } - session := a.sessions.Get(code) - if session == nil { - return "", invalidCodeError - } - return session.Scope, nil -} - -func generateCode() string { - buf := make([]byte, 128/8) - _, err := rand.Read(buf) - if err != nil { - panic(err) - } - return base64.URLEncoding.EncodeToString(buf) -} diff --git a/auth/api/iam/openid4vp.go b/auth/api/iam/openid4vp.go index 30f467a85a..b282b4f5f8 100644 --- a/auth/api/iam/openid4vp.go +++ b/auth/api/iam/openid4vp.go @@ -24,6 +24,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/google/uuid" "github.com/labstack/echo/v4" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" @@ -33,8 +34,11 @@ import ( "net/http" "net/url" "strings" + "time" ) +const sessionExpiry = 5 * time.Minute + // createPresentationRequest creates a new Authorization Request as specified by OpenID4VP: https://openid.net/specs/openid-4-verifiable-presentations-1_0.html. // It is sent by a verifier to a wallet, to request one or more verifiable credentials as verifiable presentation from the wallet. func (r *Wrapper) sendPresentationRequest(ctx context.Context, response http.ResponseWriter, scope string, @@ -145,7 +149,12 @@ func (r *Wrapper) handlePresentationRequest(params map[string]string, session *S } session.ServerState["openid4vp_credentials"] = credentialIDs - templateParams.SessionID = r.sessions.Create(*session) + sessionID := uuid.NewString() + err = r.storageEngine.GetSessionDatabase().GetStore(sessionExpiry, session.OwnDID.String(), "session").Put(sessionID, *session) + if err != nil { + return nil, err + } + templateParams.SessionID = sessionID // TODO: Support multiple languages buf := new(bytes.Buffer) @@ -162,12 +171,16 @@ func (r *Wrapper) handlePresentationRequest(params map[string]string, session *S // handleAuthConsent handles the authorization consent form submission. func (r *Wrapper) handlePresentationRequestAccept(c echo.Context) error { // TODO: Needs authentication? - var session *Session - if sessionID := c.FormValue("sessionID"); sessionID != "" { - session = r.sessions.Get(sessionID) + sessionID := c.FormValue("sessionID") + if sessionID == "" { + return errors.New("missing sessionID parameter") } - if session == nil { - return errors.New("invalid session") + + var session Session + sessionStore := r.storageEngine.GetSessionDatabase().GetStore(sessionExpiry, "openid", session.OwnDID.String(), "session") + err := sessionStore.Get(sessionID, &session) + if err != nil { + return fmt.Errorf("invalid session: %w", err) } // TODO: Change to loading from wallet diff --git a/auth/api/iam/openid4vp_test.go b/auth/api/iam/openid4vp_test.go index 91fec61bfd..22920d4a92 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/storage" "github.com/nuts-foundation/nuts-node/vcr" "github.com/nuts-foundation/nuts-node/vcr/credential" "github.com/nuts-foundation/nuts-node/vcr/holder" @@ -42,7 +43,7 @@ var holderDID = did.MustParseDID("did:web:example.com:holder") var issuerDID = did.MustParseDID("did:web:example.com:issuer") func TestWrapper_sendPresentationRequest(t *testing.T) { - instance := New(nil, nil, nil) + instance := New(nil, nil, nil, nil) redirectURI, _ := url.Parse("https://example.com/redirect") verifierID, _ := url.Parse("https://example.com/verifier") @@ -101,7 +102,7 @@ func TestWrapper_handlePresentationRequest(t *testing.T) { mockAuth.EXPECT().PresentationDefinitions().Return(peStore) mockWallet.EXPECT().List(gomock.Any(), holderDID).Return(walletCredentials, nil) mockVDR.EXPECT().IsOwner(gomock.Any(), holderDID).Return(true, nil) - instance := New(mockAuth, mockVCR, mockVDR) + instance := New(mockAuth, mockVCR, mockVDR, storage.NewTestStorageEngine(t)) params := map[string]string{ "scope": "eOverdracht-overdrachtsbericht", @@ -124,7 +125,7 @@ func TestWrapper_handlePresentationRequest(t *testing.T) { _ = peStore.LoadFromFile("test/presentation_definition_mapping.json") mockAuth := auth.NewMockAuthenticationServices(ctrl) mockAuth.EXPECT().PresentationDefinitions().Return(peStore) - instance := New(mockAuth, nil, nil) + instance := New(mockAuth, nil, nil, nil) params := map[string]string{ "scope": "unsupported", @@ -139,7 +140,7 @@ func TestWrapper_handlePresentationRequest(t *testing.T) { assert.Nil(t, response) }) t.Run("invalid response_mode", func(t *testing.T) { - instance := New(nil, nil, nil) + instance := New(nil, nil, nil, nil) params := map[string]string{ "scope": "eOverdracht-overdrachtsbericht", "response_type": "code", diff --git a/auth/api/iam/session.go b/auth/api/iam/session.go index de6629f458..f72a4c1d56 100644 --- a/auth/api/iam/session.go +++ b/auth/api/iam/session.go @@ -19,34 +19,10 @@ package iam import ( - "github.com/google/uuid" "github.com/nuts-foundation/go-did/did" "net/url" - "sync" ) -type SessionManager struct { - sessions *sync.Map -} - -func (s *SessionManager) Create(session Session) string { - // TODO: Session expiration - // TODO: Session storage - // TODO: Session pinning and other safety measures (see OAuth2 Threat Model) - id := uuid.NewString() - s.sessions.Store(id, session) - return id -} - -func (s *SessionManager) Get(id string) *Session { - session, ok := s.sessions.Load(id) - if !ok { - return nil - } - result := session.(Session) - return &result -} - type Session struct { ClientID string Scope string diff --git a/cmd/root.go b/cmd/root.go index a01eaaa9f6..4c3c6b302e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -214,7 +214,7 @@ func CreateSystem(shutdownCallback context.CancelFunc) *core.System { system.RegisterRoutes(statusEngine.(core.Routable)) system.RegisterRoutes(metricsEngine.(core.Routable)) system.RegisterRoutes(&authAPIv1.Wrapper{Auth: authInstance, CredentialResolver: credentialInstance}) - system.RegisterRoutes(authIAMAPI.New(authInstance, credentialInstance, vdrInstance)) + system.RegisterRoutes(authIAMAPI.New(authInstance, credentialInstance, vdrInstance, storageInstance)) system.RegisterRoutes(&authMeansAPI.Wrapper{Auth: authInstance}) system.RegisterRoutes(&didmanAPI.Wrapper{Didman: didmanInstance})