Skip to content

Commit

Permalink
rebase fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
reinkrul committed Oct 6, 2023
1 parent 1a82480 commit 990fe6b
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 58 deletions.
78 changes: 61 additions & 17 deletions auth/api/iam/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ package iam
import (
"bytes"
"context"
"crypto/rand"
"embed"
"encoding/base64"
"errors"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/nuts-node/audit"
Expand Down Expand Up @@ -88,21 +91,21 @@ func (r Wrapper) Routes(router core.EchoRouter) {
auditMiddleware := audit.Middleware(apiModuleName)
// The following handler is of the OpenID4VCI wallet which is called by the holder (wallet owner)
// when accepting an OpenID4VP authorization request.
router.POST("/iam/:did/openid_authz_accept", r.handlePresentationRequestAccept, auditMiddleware)
router.POST("/iam/:id/openid_authz_accept", r.handlePresentationRequestAccept, auditMiddleware)
// The following handler is of the OpenID4VP verifier where the wallet can retrieve the Authorization Request Object,
// as specified by https://www.rfc-editor.org/rfc/rfc9101.txt
router.GET("/iam/openid4vp/authzreq/:sessionID", r.getOpenID4VPAuthzRequest, auditMiddleware)
router.GET("/iam/:id/openid/request/:sessionID", r.handleGetOpenIDRequestObject, auditMiddleware)
// The following handler is of the OpenID4VP or SIOPv2 verifier where the user-agent can retrieve the session object,
// which can be used to retrieve the Authorization Response.
router.GET("/iam/openid/session/:sessionID", r.getOpenIDSession, auditMiddleware)
router.GET("/iam/:id/openid/session/:sessionID", r.handleGetOpenIDSession, auditMiddleware)
// The following handlers are used to test/demo the OpenID4VP flows.
// - GET /openid_demo: renders an HTML page with a form to start the SIOPv2/OpenID4VP flow (POST handles form submission).
// - GET /openid_demo_completed: renders an HTML page with the result of the flow
// - GET /openid4vp_demo_status: API for XIS to retrieve the status of the flow (if sessionID param is present)
router.GET("/iam/openid_demo", r.handleOpenIDDemoStart, auditMiddleware)
router.GET("/iam/openid_demo_completed", r.handleOpenIDDemoCompleted, auditMiddleware)
router.POST("/iam/openid_demo", r.handleOpenID4VPDemoSendRequest, auditMiddleware)
router.GET("/iam/:did/openid_demo_status", r.handleOpenID4VPDemoRequestWalletStatus, auditMiddleware)
router.GET("/iam/:id/openid_demo_completed", r.handleOpenIDDemoCompleted, auditMiddleware)
router.GET("/iam/:id/openid_demo_status", r.handleOpenID4VPDemoRequestWalletStatus, auditMiddleware)
}

func (r Wrapper) middleware(ctx echo.Context, request interface{}, operationID string, f StrictHandlerFunc) (interface{}, error) {
Expand Down Expand Up @@ -207,12 +210,12 @@ func (r Wrapper) HandleAuthorizeRequest(ctx context.Context, request HandleAutho
}

func (r Wrapper) HandleAuthorizeRedirectResponse(ctx context.Context, authResponse HandleAuthorizeRedirectResponseRequestObject) (HandleAuthorizeRedirectResponseResponseObject, error) {
_ = idToDID(authResponse.Id)
ownDID := idToDID(authResponse.Id)
// TODO: IsOwner
// For now, only query parameters are supported
httpRequest := ctx.Value(httpRequestContextKey).(*http.Request)

session, err := r.handleAuthorizeResponse(httpRequest.URL.Query())
session, err := r.handleAuthorizeResponse(ownDID, httpRequest.URL.Query())
if err != nil {
// TODO: render error HTML page for the browser
return nil, err
Expand Down Expand Up @@ -246,7 +249,7 @@ func (r Wrapper) HandleAuthorizeRedirectResponse(ctx context.Context, authRespon
}

func (r Wrapper) HandleAuthorizeResponse(_ context.Context, authResponse HandleAuthorizeResponseRequestObject) (HandleAuthorizeResponseResponseObject, error) {
_ = idToDID(authResponse.Id)
ownDID := idToDID(authResponse.Id)
// TODO: IsOwner
requestData, err := io.ReadAll(authResponse.Body)
if err != nil {
Expand All @@ -265,7 +268,7 @@ func (r Wrapper) HandleAuthorizeResponse(_ context.Context, authResponse HandleA
Description: err.Error(),
}
}
_, err = r.handleAuthorizeResponse(params)
_, err = r.handleAuthorizeResponse(ownDID, params)
if err != nil {
return nil, err
}
Expand All @@ -275,14 +278,17 @@ func (r Wrapper) HandleAuthorizeResponse(_ context.Context, authResponse HandleA
}, nil
}

func (r Wrapper) handleAuthorizeResponse(params url.Values) (*Session, error) {
sessionID, session, err := r.getSession(params)
func (r Wrapper) handleAuthorizeResponse(ownDID did.DID, params url.Values) (*Session, error) {
sessionID, session, err := r.getSessionFromParams(ownDID, params)
if err != nil {
return nil, err
}
err = r.handleOpenIDAuthzResponse(session, params)
if err == nil {
r.sessions.Update(sessionID, *session)
err := r.updateSession(ownDID, sessionID, *session)
if err != nil {
return nil, err
}
}
return session, err
}
Expand Down Expand Up @@ -355,22 +361,51 @@ func createSession(params map[string]string, ownDID did.DID) *Session {
return session
}

func (r Wrapper) getSession(params url.Values) (string, *Session, error) {
func (r Wrapper) getSessionByID(ownDID did.DID, sessionID string) (*Session, error) {
var session Session
sessionStore := r.storageEngine.GetSessionDatabase().GetStore(sessionExpiry, "openid", ownDID.String(), "session")
err := sessionStore.Get(sessionID, &session)
if errors.Is(err, storage.ErrNotFound) {
return nil, OAuth2Error{
Code: InvalidRequest,
Description: "unknown/expired session",
}
} else if err != nil {
return nil, err
}
return &session, nil
}

func (r Wrapper) getSessionFromParams(ownDID did.DID, params url.Values) (string, *Session, error) {
sessionID := params.Get("state")
if sessionID == "" {
return "", nil, OAuth2Error{
Code: InvalidRequest,
Description: "missing state parameter",
}
}
session := r.sessions.Get(sessionID)
if session == nil {
return "", nil, OAuth2Error{
session, err := r.getSessionByID(ownDID, sessionID)
if err != nil {
return "", nil, err
}
return sessionID, session, nil
}

func (r Wrapper) createSession(ownDID did.DID, session Session) (string, error) {
sessionStore := r.storageEngine.GetSessionDatabase().GetStore(sessionExpiry, "openid", ownDID.String(), "session")
id := uuid.NewString()
return id, sessionStore.Put(id, session)
}

func (r Wrapper) updateSession(ownDID did.DID, sessionID string, session Session) error {
sessionStore := r.storageEngine.GetSessionDatabase().GetStore(sessionExpiry, "openid", ownDID.String(), "session")
if !sessionStore.Exists(sessionID) {
return OAuth2Error{
Code: InvalidRequest,
Description: "unknown/expired session",
}
}
return sessionID, session, nil
return sessionStore.Put(sessionID, session)
}

func idToDID(id string) did.DID {
Expand All @@ -381,3 +416,12 @@ func idToDID(id string) did.DID {
DecodedID: id,
}
}

func generateCode() string {
buf := make([]byte, 128/8)
_, err := rand.Read(buf)
if err != nil {
panic(err)
}
return base64.URLEncoding.EncodeToString(buf)
}
2 changes: 1 addition & 1 deletion auth/api/iam/assets/openid_demo_qrcode.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ <h1>OpenID4VP Demo (Verifier)</h1>
.then(response => response.json())
.then(data => {
if (data.id_token || data.vp_token) {
window.location.href = "/iam/openid_demo_completed?state={{ .SessionID }}";
window.location.href = "/iam/{{ .ID }}/openid_demo_completed?state={{ .SessionID }}";
}
});
}, 2000)
Expand Down
43 changes: 17 additions & 26 deletions auth/api/iam/openid.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const sessionExpiry = 5 * time.Minute

// createOpenIDAuthzRequest 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) createOpenIDAuthzRequest(ctx context.Context, scope string, state string, presentationDefinition pe.PresentationDefinition, responseTypes []string, redirectURL url.URL, verifierDID did.DID) (string, error) {
func (r Wrapper) createOpenIDAuthzRequest(ctx context.Context, scope string, state string, presentationDefinition pe.PresentationDefinition, responseTypes []string, redirectURL url.URL, verifierDID did.DID) (string, error) {
params := make(map[string]interface{})
params[scopeParam] = scope
params[redirectURIParam] = redirectURL.String()
Expand Down Expand Up @@ -122,7 +122,7 @@ func (r *Wrapper) createOpenIDAuthzRequest(ctx context.Context, scope string, st

// sendPresentationRequest creates a new OpenID4VP Presentation Requests and "sends" it to the wallet, by redirecting the user-agent to the wallet's authorization endpoint.
// 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,
func (r Wrapper) sendPresentationRequest(ctx context.Context, response http.ResponseWriter, scope []string,
redirectURL url.URL, verifierIdentifier url.URL, walletIdentifier url.URL) error {
// TODO: Lookup wallet metadata for correct authorization endpoint. But for Nuts nodes, we derive it from the walletIdentifier
authzEndpoint := walletIdentifier.JoinPath("/authorize")
Expand All @@ -143,7 +143,7 @@ func (r *Wrapper) sendPresentationRequest(ctx context.Context, response http.Res

// handlePresentationRequest handles an Authorization Request as specified by OpenID4VP: https://openid.net/specs/openid-4-verifiable-presentations-1_0.html.
// It is handled by a wallet, called by a verifier who wants the wallet to present one or more verifiable credentials.
func (r *Wrapper) handlePresentationRequest(params map[string]string, session *Session) (HandleAuthorizeRequestResponseObject, error) {
func (r Wrapper) handlePresentationRequest(params map[string]string, session *Session) (HandleAuthorizeRequestResponseObject, error) {
ctx := context.TODO()
// Presentation definition is always derived from the scope.
// Later on, we might support presentation_definition and/or presentation_definition_uri parameters instead of scope as well.
Expand Down Expand Up @@ -250,18 +250,12 @@ 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 {
func (r Wrapper) handlePresentationRequestAccept(c echo.Context) error {
ownDID := idToDID(c.Param("id"))
// TODO: Needs authentication?
sessionID := c.FormValue("sessionID")
if sessionID == "" {
return errors.New("missing sessionID parameter")
}

var session Session
sessionStore := r.storageEngine.GetSessionDatabase().GetStore(sessionExpiry, "openid", session.OwnDID.String(), "session")
err := sessionStore.Get(sessionID, &session)
session, err := r.getSessionByID(ownDID, c.FormValue("sessionID"))
if err != nil {
return fmt.Errorf("invalid session: %w", err)
return err
}

// TODO: Change to loading from wallet
Expand Down Expand Up @@ -363,23 +357,20 @@ func (r Wrapper) handleOpenID4VPAuthzResponse(session *Session, params url.Value
return nil
}

func (r Wrapper) getOpenID4VPAuthzRequest(echoCtx echo.Context) error {
sessionID := echoCtx.Param("sessionID")
if sessionID == "" {
return echoCtx.String(http.StatusBadRequest, "missing sessionID")
func (r Wrapper) handleGetOpenIDRequestObject(echoCtx echo.Context) error {
ownID := idToDID(echoCtx.Param("id"))
session, err := r.getSessionByID(ownID, echoCtx.Param("sessionID"))
if err != nil {
return err
}
session := r.sessions.Get(sessionID)
return echoCtx.String(http.StatusOK, session.RequestObject)
}

func (r Wrapper) getOpenIDSession(echoCtx echo.Context) error {
sessionID := echoCtx.Param("sessionID")
if sessionID == "" {
return echoCtx.String(http.StatusBadRequest, "missing sessionID")
}
session := r.sessions.Get(sessionID)
if session == nil {
return echoCtx.String(http.StatusNotFound, "unknown/expired session")
func (r Wrapper) handleGetOpenIDSession(echoCtx echo.Context) error {
ownID := idToDID(echoCtx.Param("id"))
session, err := r.getSessionByID(ownID, echoCtx.Param("sessionID"))
if err != nil {
return err
}
return echoCtx.JSON(http.StatusOK, session)
}
Expand Down
37 changes: 23 additions & 14 deletions auth/api/iam/openid_demo.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import (
"strings"
)

func (r *Wrapper) handleOpenIDDemoStart(echoCtx echo.Context) error {
func (r Wrapper) handleOpenIDDemoStart(echoCtx echo.Context) error {
ownedDIDs, _ := r.vdr.ListOwned(echoCtx.Request().Context())
if len(ownedDIDs) == 0 {
return errors.New("no owned DIDs")
Expand All @@ -52,7 +52,11 @@ func (r *Wrapper) handleOpenIDDemoStart(echoCtx echo.Context) error {
}

func (r Wrapper) handleOpenIDDemoCompleted(c echo.Context) error {
_, session, err := r.getSession(c.QueryParams())
ownID := idToDID(c.Param("id"))
_, session, err := r.getSessionFromParams(ownID, c.QueryParams())
if err != nil {
return err
}
if err != nil {
return err
}
Expand All @@ -78,7 +82,7 @@ func (r Wrapper) handleOpenIDDemoCompleted(c echo.Context) error {
return c.HTML(http.StatusOK, buf.String())
}

func (r *Wrapper) handleOpenID4VPDemoSendRequest(echoCtx echo.Context) error {
func (r Wrapper) handleOpenID4VPDemoSendRequest(echoCtx echo.Context) error {
verifierID := echoCtx.FormValue("verifier_id")
if verifierID == "" {
return errors.New("missing verifier_id")
Expand Down Expand Up @@ -112,7 +116,6 @@ func (r *Wrapper) handleOpenID4VPDemoSendRequest(echoCtx echo.Context) error {
OwnDID: *verifierDID,
ResponseType: []string{responseTypeIDToken},
}
sessionID := r.sessions.Create(session)
pePurpose := "For this demo you can provide any credential"
pattern := "Sphereon Guest"
presentationDefinition := pe.PresentationDefinition{
Expand Down Expand Up @@ -145,24 +148,32 @@ func (r *Wrapper) handleOpenID4VPDemoSendRequest(echoCtx echo.Context) error {
},
},
}
sessionID, err := r.createSession(*verifierDID, session)
if err != nil {
return fmt.Errorf("failed to create session: %w", err)
}
requestObject, err := r.createOpenIDAuthzRequest(ctx, scope, sessionID, presentationDefinition,
session.ResponseType, *verifierURL.JoinPath("authresponse"), *verifierDID)
if err != nil {
return fmt.Errorf("failed to create request object: %w", err)
}
session.RequestObject = requestObject
r.sessions.Update(sessionID, session)
if err := r.updateSession(*verifierDID, sessionID, session); err != nil {
return fmt.Errorf("failed to update session: %w", err)
}

requestURI := r.auth.PublicURL().JoinPath("iam", "openid4vp", "authzreq", sessionID)
requestURI := r.auth.PublicURL().JoinPath("iam", verifierDID.String(), "openid", "request", sessionID)
// openid-vc is JWT VC Presentation Profile scheme?
qrCode := "openid-vc://?" + url.Values{"request_uri": []string{requestURI.String()}}.Encode()

// Show QR code to scan using (mobile) wallet
buf := new(bytes.Buffer)
if err := r.templates.ExecuteTemplate(buf, "openid_demo_qrcode.html", struct {
ID string
SessionID string
QRCode string
}{
ID: verifierDID.ID,
SessionID: sessionID,
QRCode: qrCode,
}); err != nil {
Expand All @@ -172,14 +183,12 @@ func (r *Wrapper) handleOpenID4VPDemoSendRequest(echoCtx echo.Context) error {
}
}

func (r *Wrapper) handleOpenID4VPDemoRequestWalletStatus(echoCtx echo.Context) error {
sessionID := echoCtx.FormValue("sessionID")
if sessionID == "" {
return echoCtx.JSON(http.StatusBadRequest, "missing sessionID")
}
session := r.sessions.Get(sessionID)
if session == nil {
return echoCtx.JSON(http.StatusNotFound, "unknown session")
func (r Wrapper) handleOpenID4VPDemoRequestWalletStatus(echoCtx echo.Context) error {
ownDID := idToDID(echoCtx.Param("id"))
// TODO: Needs authentication?
session, err := r.getSessionByID(ownDID, echoCtx.FormValue("sessionID"))
if err != nil {
return err
}
if session.IDToken == nil {
// No VP yet, keep polling
Expand Down

0 comments on commit 990fe6b

Please sign in to comment.