Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
woutslakhorst committed Dec 13, 2023
1 parent 8b33dec commit f7f54be
Show file tree
Hide file tree
Showing 15 changed files with 615 additions and 19 deletions.
7 changes: 7 additions & 0 deletions auth/api/auth/v1/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ type mockAuthClient struct {
authzServer *oauth.MockAuthorizationServer
relyingParty *oauth.MockRelyingParty
verifier *oauth.MockVerifier
holder *oauth.MockHolder
}

func (m *mockAuthClient) V2APIEnabled() bool {
Expand All @@ -85,6 +86,10 @@ func (m *mockAuthClient) Verifier() oauth.Verifier {
return m.verifier
}

func (m *mockAuthClient) Holder() oauth.Holder {
return m.holder
}

func (m *mockAuthClient) ContractNotary() services.ContractNotary {
return m.contractNotary
}
Expand All @@ -105,13 +110,15 @@ func createContext(t *testing.T) *TestContext {
relyingParty := oauth.NewMockRelyingParty(ctrl)
verifier := oauth.NewMockVerifier(ctrl)
mockCredentialResolver := vcr.NewMockResolver(ctrl)
holder := oauth.NewMockHolder(ctrl)

authMock := &mockAuthClient{
ctrl: ctrl,
contractNotary: contractNotary,
authzServer: authzServer,
relyingParty: relyingParty,
verifier: verifier,
holder: holder,
}

requestCtx := audit.TestContext()
Expand Down
4 changes: 1 addition & 3 deletions auth/api/iam/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,9 +295,7 @@ func (r Wrapper) HandleAuthorizeRequest(ctx context.Context, request HandleAutho
case responseTypeVPToken:
// Options:
// - OpenID4VP flow, vp_token is sent in Authorization Response
// TODO: Check parameters for right flow
// TODO: Do we actually need this? (probably not)
panic("not implemented")
return r.handleAuthorizeRequestFromVerifier(ctx, *ownDID, params)
case responseTypeVPIDToken:
// Options:
// - OpenID4VP+SIOP flow, vp_token is sent in Authorization Response
Expand Down
167 changes: 161 additions & 6 deletions auth/api/iam/openid4vp.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,24 @@ import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"

"github.com/google/uuid"
"github.com/labstack/echo/v4"
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"
oauth2 "github.com/nuts-foundation/nuts-node/auth/services/oauth"
"github.com/nuts-foundation/nuts-node/crypto"
httpNuts "github.com/nuts-foundation/nuts-node/http"
"github.com/nuts-foundation/nuts-node/network/log"
"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/pe"
"github.com/nuts-foundation/nuts-node/vdr/didweb"
"net/http"
"net/url"
"strings"
)

var oauthNonceKey = []string{"oauth", "nonce"}
Expand Down Expand Up @@ -104,6 +106,8 @@ func (r Wrapper) handleAuthorizeRequestFromHolder(ctx context.Context, verifier
// like this:
// GET /authorize?
// response_type=vp_token
// &client_id_scheme=did
// &client_metadata_uri=https%3A%2F%2Fexample.com%2Fiam%2F123%2F%2Fclient_metadata
// &client_id=did:web:example.com:iam:123
// &client_id_scheme=did
// &client_metadata_uri=https%3A%2F%2Fexample.com%2F.well-known%2Fauthorization-server%2Fiam%2F123%2F%2F
Expand Down Expand Up @@ -131,8 +135,8 @@ func (r Wrapper) handleAuthorizeRequestFromHolder(ctx context.Context, verifier

authServerURL := httpNuts.AddQueryParams(*walletURL, map[string]string{
responseTypeParam: responseTypeVPToken,
clientIDParam: verifier.String(),
clientIDSchemeParam: didScheme,
clientIDParam: verifier.String(),
responseURIParam: callbackURL.String(),
presentationDefUriParam: presentationDefinitionURI.String(),
clientMetadataURIParam: metadataURL.String(),
Expand All @@ -158,6 +162,157 @@ func (r Wrapper) handleAuthorizeRequestFromHolder(ctx context.Context, verifier
}, nil
}

func (r Wrapper) handleAuthorizeRequestFromVerifier(ctx context.Context, walletDID did.DID, params map[string]string) (HandleAuthorizeRequestResponseObject, error) {
// we expect an OpenID4VP request like this
// GET /iam/456/authorize?response_type=vp_token&client_id=did:web:example.com:iam:123&nonce=xyz
// &response_mode=direct_post&response_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb&presentation_definition_uri=example.com%2Fiam%2F123%2Fpresentation_definition?scope=a+b HTTP/1.1
// Host: server.com
// The following parameters are expected
// response_type, REQUIRED. Value MUST be set to "vp_token".
// client_id, REQUIRED. This must be a did:web
// response_uri, REQUIRED. This must be the verifier node url
// response_mode, REQUIRED. Value MUST be "direct_post"
// state, REQUIRED. Original client state from the holder (in the client role)
// presentation_definition_uri, REQUIRED. For getting the presentation definition

// check the response URL because later errors will redirect to this URL
responseURI, responseOK := params[responseURIParam]

// get the original authorization request of the client, if something fails we need the redirectURI from this request
// get the state parameter
state, ok := params[stateParam]
if !ok && !responseOK {
// log and render error page
log.Logger().Error("handleAuthorizeRequestFromVerifier is missing state and response_uri parameter")
// this goes to the user-agent :(
return nil, oauthError(oauth.ServerError, "something went wrong", nil)
}
if !ok {
// post error to responseURI, if it fails, it'll render error page
return r.sendAndHandleDirectPostError(ctx, oauthError(oauth.InvalidRequest, "missing state parameter", nil), responseURI)
}

// check client state
// if no state, post error
var session OAuthSession
err := r.oauthClientStateStore().Get(state, &session)
if err != nil {
return r.sendAndHandleDirectPostError(ctx, oauthError(oauth.InvalidRequest, "state has expired", nil), responseURI)
}
clientRedirectURL := session.redirectURI()

verifierID, ok := params[clientIDParam]
if !ok {
return r.sendAndHandleDirectPostError(ctx, oauthError(oauth.InvalidRequest, "missing client_id parameter", clientRedirectURL), responseURI)
}
// the verifier must be a did:web
verifierDID, err := did.ParseDID(verifierID)
if err != nil || verifierDID.Method != "web" {
return r.sendAndHandleDirectPostError(ctx, oauthError(oauth.InvalidRequest, "invalid client_id parameter", clientRedirectURL), responseURI)
}
// get verifier metadata
clientMetadataURI, ok := params[clientMetadataURIParam]
if !ok {
return r.sendAndHandleDirectPostError(ctx, oauthError(oauth.InvalidRequest, "missing client_metadata_uri parameter", clientRedirectURL), responseURI)
}
// we ignore any client_metadata, but officially an error must be returned when that param is present.
metadata, err := r.auth.Holder().ClientMetadata(ctx, clientMetadataURI)
if err != nil {
return r.sendAndHandleDirectPostError(ctx, oauthError(oauth.ServerError, "failed to get client metadata (verifier)", clientRedirectURL), responseURI)
}
// get presentation_definition from presentation_definition_uri
presentationDefinitionURI, ok := params[presentationDefUriParam]
if !ok {
return r.sendAndHandleDirectPostError(ctx, oauthError(oauth.InvalidRequest, "missing presentation_definition_uri parameter", clientRedirectURL), responseURI)
}
presentationDefinition, err := r.auth.RelyingParty().PresentationDefinition(ctx, presentationDefinitionURI)
if err != nil {
return r.sendAndHandleDirectPostError(ctx, oauthError(oauth.InvalidPresentationDefinitionURI, fmt.Sprintf("failed to retrieve presentation definition on %s", presentationDefinitionURI), clientRedirectURL), responseURI)
}

// check nonce
nonce, ok := params[nonceParam]
if !ok {
return r.sendAndHandleDirectPostError(ctx, oauthError(oauth.InvalidRequest, "missing nonce parameter", clientRedirectURL), responseURI)
}

// at this point in the flow it would be possible to ask the user to confirm the credentials to use

// all params checked, delegate responsibility to the holder
vp, submission, err := r.auth.Holder().BuildPresentation(ctx, walletDID, *presentationDefinition, *metadata, nonce)
if err != nil {
if errors.Is(err, oauth2.ErrNoCredentials) {
return r.sendAndHandleDirectPostError(ctx, oauthError(oauth.InvalidRequest, "no credentials available", clientRedirectURL), responseURI)
}
return r.sendAndHandleDirectPostError(ctx, oauthError(oauth.ServerError, err.Error(), clientRedirectURL), responseURI)
}

return r.sendAndHandleDirectPost(ctx, *vp, *submission, responseURI, *clientRedirectURL)
}

// sendAndHandleDirectPost sends OpenID4VP direct_post to the verifier. The verifier responds with a redirect to the client (including error fields if needed).
// If the direct post fails, the user-agent will be redirected back to the client with an error. (Original redirect_uri).
// If no redirect_uri is present, the user-agent will be redirected to the error page.
func (r Wrapper) sendAndHandleDirectPost(ctx context.Context, vp vc.VerifiablePresentation, presentationSubmission pe.PresentationSubmission, verifierResponseURI string, clientRedirectURL url.URL) (HandleAuthorizeRequestResponseObject, error) {
redirectURI, err := r.auth.Holder().PostAuthorizationResponse(ctx, vp, presentationSubmission, verifierResponseURI)
if err == nil {
return HandleAuthorizeRequest302Response{
HandleAuthorizeRequest302ResponseHeaders{
Location: redirectURI,
},
}, nil
}

msg := fmt.Sprintf("failed to post authorization response to verifier @ %s", verifierResponseURI)
log.Logger().WithError(err).Error(msg)

// clientRedirectURI has been checked earlier in te process.
clientRedirectURL = httpNuts.AddQueryParams(clientRedirectURL, map[string]string{
oauth.ErrorParam: string(oauth.ServerError),
oauth.ErrorDescriptionParam: msg,
})
return HandleAuthorizeRequest302Response{
HandleAuthorizeRequest302ResponseHeaders{
Location: clientRedirectURL.String(),
},
}, nil
}

// sendAndHandleDirectPostError sends errors from handleAuthorizeRequestFromVerifier as direct_post to the verifier. The verifier responds with a redirect to the client (including error fields).
// If the direct post fails, the user-agent will be redirected back to the client with an error. (Original redirect_uri).
// If no redirect_uri is present, the user-agent will be redirected to the error page.
func (r Wrapper) sendAndHandleDirectPostError(ctx context.Context, auth2Error oauth.OAuth2Error, verifierResponseURI string) (HandleAuthorizeRequestResponseObject, error) {
redirectURI, err := r.auth.Holder().PostError(ctx, auth2Error, verifierResponseURI)
if err == nil {
return HandleAuthorizeRequest302Response{
HandleAuthorizeRequest302ResponseHeaders{
Location: redirectURI,
},
}, nil
}

msg := fmt.Sprintf("failed to post error to verifier @ %s", verifierResponseURI)
log.Logger().WithError(err).Error(msg)

if auth2Error.RedirectURI == nil {
// render error page because all else failed, in a correct flow this should never happen
// it could be the case that the client state has just expired, so no redirectURI is present and the verifier is not responding
log.Logger().WithError(err).Error("failed to post error to verifier and no clientRedirectURI present")
return nil, oauthError(oauth.ServerError, "something went wrong", nil)
}

// clientRedirectURL has been checked earlier in te process.
clientRedirectURL := httpNuts.AddQueryParams(*auth2Error.RedirectURI, map[string]string{
oauth.ErrorParam: string(oauth.ServerError),
oauth.ErrorDescriptionParam: msg,
})
return HandleAuthorizeRequest302Response{
HandleAuthorizeRequest302ResponseHeaders{
Location: clientRedirectURL.String(),
},
}, nil
}

// 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,
Expand All @@ -181,7 +336,7 @@ func (r Wrapper) sendPresentationRequest(ctx context.Context, response http.Resp

// 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 *OAuthSession) (HandleAuthorizeRequestResponseObject, error) {
func (r Wrapper) handlePresentationRequest(params map[string]string, session *OAuthSession) (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
10 changes: 5 additions & 5 deletions auth/api/iam/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ var oauthClientStateKey = []string{"oauth", "client_state"}
var userRedirectSessionKey = []string{"user", "redirect"}
var userSessionKey = []string{"user", "session"}

func (r *Wrapper) requestUserAccessToken(_ context.Context, requester did.DID, request RequestAccessTokenRequestObject) (RequestAccessTokenResponseObject, error) {
func (r Wrapper) requestUserAccessToken(_ context.Context, requester did.DID, request RequestAccessTokenRequestObject) (RequestAccessTokenResponseObject, error) {
// generate a redirect token valid for 5 seconds
token := crypto.GenerateNonce()
store := r.userRedirectStore()
Expand Down Expand Up @@ -78,7 +78,7 @@ func (r *Wrapper) requestUserAccessToken(_ context.Context, requester did.DID, r

// handleUserLanding is the handler for the landing page of the user.
// It renders the page with the correct context based on the token.
func (r *Wrapper) handleUserLanding(echoCtx echo.Context) error {
func (r Wrapper) handleUserLanding(echoCtx echo.Context) error {
// todo: user authentication is currently not implemented, user consent is not implemented
// This means that this handler succeeds if the token is valid
// It only checks for an existing RequestAccessTokenRequestObject in the store
Expand Down Expand Up @@ -136,14 +136,14 @@ func (r *Wrapper) handleUserLanding(echoCtx echo.Context) error {
return echoCtx.Redirect(http.StatusFound, redirectURL.String())
}

func (r *Wrapper) userRedirectStore() storage.SessionStore {
func (r Wrapper) userRedirectStore() storage.SessionStore {
return r.storageEngine.GetSessionDatabase().GetStore(userRedirectTimeout, userRedirectSessionKey...)
}

func (r *Wrapper) userSessionStore() storage.SessionStore {
func (r Wrapper) userSessionStore() storage.SessionStore {
return r.storageEngine.GetSessionDatabase().GetStore(userSessionTimeout, userSessionKey...)
}

func (r *Wrapper) oauthClientStateStore() storage.SessionStore {
func (r Wrapper) oauthClientStateStore() storage.SessionStore {
return r.storageEngine.GetSessionDatabase().GetStore(oAuthFlowTimeout, oauthClientStateKey...)
}
12 changes: 9 additions & 3 deletions auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type Auth struct {
authzServer oauth.AuthorizationServer
relyingParty oauth.RelyingParty
verifier oauth.Verifier
holder oauth.Holder
contractNotary services.ContractNotary
serviceResolver didman.CompoundServiceResolver
keyStore crypto.KeyStore
Expand Down Expand Up @@ -119,6 +120,10 @@ func (auth *Auth) Verifier() oauth.Verifier {
return auth.verifier
}

func (auth *Auth) Holder() oauth.Holder {
return auth.holder
}

// Configure the Auth struct by creating a validator and create an Irma server
func (auth *Auth) Configure(config core.ServerConfig) error {
if auth.config.Irma.SchemeManager == "" {
Expand Down Expand Up @@ -165,13 +170,14 @@ func (auth *Auth) Configure(config core.ServerConfig) error {
return fmt.Errorf("failed to load presentation exchange mapping file: %w", err)
}
}

clientTimeout := time.Duration(auth.config.HTTPTimeout) * time.Second
accessTokenLifeSpan := time.Duration(auth.config.AccessTokenLifeSpan) * time.Second
auth.authzServer = oauth.NewAuthorizationServer(auth.vdrInstance.Resolver(), auth.vcr, auth.vcr.Verifier(), auth.serviceResolver,
auth.keyStore, auth.contractNotary, auth.jsonldManager, accessTokenLifeSpan)
auth.relyingParty = oauth.NewRelyingParty(auth.vdrInstance.Resolver(), auth.serviceResolver,
auth.keyStore, auth.vcr.Wallet(), time.Duration(auth.config.HTTPTimeout)*time.Second, tlsConfig, config.Strictmode)
auth.verifier = oauth.NewVerifier(config.Strictmode, time.Duration(auth.config.HTTPTimeout)*time.Second, tlsConfig)
auth.keyStore, auth.vcr.Wallet(), clientTimeout, tlsConfig, config.Strictmode)
auth.verifier = oauth.NewVerifier(config.Strictmode, clientTimeout, tlsConfig)
auth.holder = oauth.NewHolder(auth.vcr.Wallet(), config.Strictmode, clientTimeout, tlsConfig)

if err := auth.authzServer.Configure(auth.config.ClockSkew, config.Strictmode); err != nil {
return err
Expand Down
Loading

0 comments on commit f7f54be

Please sign in to comment.