Skip to content

Commit

Permalink
added authorize endpoint as specified by rfc6549 authorization code
Browse files Browse the repository at this point in the history
test fixes

reduce duplicate code

correct idtoDID conversion

extended goldenhammer with key alaising

hammer tests back to green

other tests to green
  • Loading branch information
woutslakhorst committed Dec 6, 2023
1 parent 03e0eae commit 43b7105
Show file tree
Hide file tree
Showing 19 changed files with 477 additions and 159 deletions.
83 changes: 51 additions & 32 deletions auth/api/iam/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ func (r Wrapper) HandleTokenRequest(_ context.Context, request HandleTokenReques
}

// IntrospectAccessToken allows the resource server (XIS/EHR) to introspect details of an access token issued by this node
func (r Wrapper) IntrospectAccessToken(ctx context.Context, request IntrospectAccessTokenRequestObject) (IntrospectAccessTokenResponseObject, error) {
func (r Wrapper) IntrospectAccessToken(_ context.Context, request IntrospectAccessTokenRequestObject) (IntrospectAccessTokenResponseObject, error) {
// Validate token
if request.Body.Token == "" {
// Return 200 + 'Active = false' when token is invalid or malformed
Expand Down Expand Up @@ -232,9 +232,10 @@ func toAnyMap(input any) (*map[string]any, error) {

// HandleAuthorizeRequest handles calls to the authorization endpoint for starting an authorization code flow.
func (r Wrapper) HandleAuthorizeRequest(ctx context.Context, request HandleAuthorizeRequestRequestObject) (HandleAuthorizeRequestResponseObject, error) {
// TODO: must be web DID once web DID creation and DB are implemented
ownDID := idToNutsDID(request.Id)
// Create session object to be passed to handler
ownDID, err := r.idToOwnedDID(ctx, request.Id)
if err != nil {
return nil, err
}

// Workaround: deepmap codegen doesn't support dynamic query parameters.
// See https://github.com/deepmap/oapi-codegen/issues/1129
Expand All @@ -243,7 +244,7 @@ func (r Wrapper) HandleAuthorizeRequest(ctx context.Context, request HandleAutho
for key, value := range httpRequest.URL.Query() {
params[key] = value[0]
}
session := createSession(params, ownDID)
session := createSession(params, *ownDID)
if session.RedirectURI == "" {
// 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.
Expand All @@ -253,15 +254,31 @@ func (r Wrapper) HandleAuthorizeRequest(ctx context.Context, request HandleAutho
Description: "redirect_uri is required",
}
}
// todo: store session in database?

switch session.ResponseType {
case responseTypeCode:
// Options:
// - Regular authorization code flow for EHR data access through access token, authentication of end-user using OpenID4VP.
// - OpenID4VCI; authorization code flow for credential issuance to (end-user) wallet
// - OpenID4VP, vp_token is sent in Token Response; authorization code flow for presentation exchange (not required a.t.m.)
// TODO: Switch on parameters to right flow
panic("not implemented")

// TODO: officially flow switching has to be determined by the client_id
// registered client_ids should list which flow they support
// client registration could be done via rfc7591....
// for now we switch on client_id format.
// when client_id is a did:web, it is a cloud/server wallet
// otherwise it's a normal registered client which we do not support yet
// Note: this is the user facing OpenID4VP flow with a "vp_token" responseType, the demo uses the "vp_token id_token" responseType
clientId := session.ClientID
if strings.HasPrefix(clientId, "did:web:") {
// client is a cloud wallet with user
return r.handleAuthorizeRequestFromHolder(ctx, *ownDID, params)
} else {
return nil, oauth.OAuth2Error{
Code: oauth.InvalidRequest,
Description: "client_id must be a did:web",
}
}
case responseTypeVPToken:
// Options:
// - OpenID4VP flow, vp_token is sent in Authorization Response
Expand All @@ -283,19 +300,10 @@ func (r Wrapper) HandleAuthorizeRequest(ctx context.Context, request HandleAutho

// OAuthAuthorizationServerMetadata returns the Authorization Server's metadata
func (r Wrapper) OAuthAuthorizationServerMetadata(ctx context.Context, request OAuthAuthorizationServerMetadataRequestObject) (OAuthAuthorizationServerMetadataResponseObject, error) {
ownDID := r.idToDID(request.Id)
owned, err := r.vdr.IsOwner(ctx, ownDID)
_, err := r.idToOwnedDID(ctx, request.Id)
if err != nil {
if resolver.IsFunctionalResolveError(err) {
return nil, core.NotFoundError("authz server metadata: %w", err)
}
log.Logger().WithField("did", ownDID.String()).Errorf("authz server metadata: failed to assert ownership of did: %s", err.Error())
return nil, core.Error(500, "authz server metadata: %w", err)
}
if !owned {
return nil, core.NotFoundError("authz server metadata: did not owned")
return nil, err
}

identity := r.auth.PublicURL().JoinPath("iam", request.Id)

return OAuthAuthorizationServerMetadata200JSONResponse(authorizationServerMetadata(*identity)), nil
Expand All @@ -317,8 +325,7 @@ func (r Wrapper) GetWebDID(_ context.Context, request GetWebDIDRequestObject) (G

// OAuthClientMetadata returns the OAuth2 Client metadata for the request.Id if it is managed by this node.
func (r Wrapper) OAuthClientMetadata(ctx context.Context, request OAuthClientMetadataRequestObject) (OAuthClientMetadataResponseObject, error) {
// TODO: must be web DID once web DID creation and DB are implemented
ownDID := idToNutsDID(request.Id)
ownDID := r.idToDID(request.Id)
owned, err := r.vdr.IsOwner(ctx, ownDID)
if err != nil {
log.Logger().WithField("did", ownDID.String()).Errorf("oauth metadata: failed to assert ownership of did: %s", err.Error())
Expand Down Expand Up @@ -365,17 +372,29 @@ func createSession(params map[string]string, ownDID did.DID) *Session {
}
return session
}
func (r Wrapper) idToDID(id string) did.DID {
url := r.auth.PublicURL().JoinPath("iam", id)
did, _ := didweb.URLToDID(*url)
return *did
}

func idToNutsDID(id string) did.DID {
return did.DID{
// should be changed to web when migrated to web DID
Method: "nuts",
ID: id,
DecodedID: id,
func (r Wrapper) idToOwnedDID(ctx context.Context, id string) (*did.DID, error) {
ownDID := r.idToDID(id)

owned, err := r.vdr.IsOwner(ctx, ownDID)
if err != nil {
if resolver.IsFunctionalResolveError(err) {
return nil, core.NotFoundError("authz server metadata: %w", err)
}
log.Logger().WithField("did", ownDID.String()).Errorf("authz server metadata: failed to assert ownership of did: %s", err.Error())
return nil, oauth.OAuth2Error{
Code: oauth.ServerError,
Description: "failed to assert ownership of did",
}
}
if !owned {
return nil, core.NotFoundError("authz server metadata: did not owned")
}
return &ownDID, nil
}

func (r Wrapper) idToDID(id string) did.DID {
url := r.auth.PublicURL().JoinPath("iam", id)
webDID, _ := didweb.URLToDID(*url)
return *webDID
}
59 changes: 44 additions & 15 deletions auth/api/iam/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ import (
"time"
)

var nutsDID = did.MustParseDID("did:nuts:123")
var webDID = did.MustParseDID("did:web:example.com:iam:123")
var webIDPart = "123"
var verifierDID = did.MustParseDID("did:web:example.com:iam:verifier")

func TestWrapper_OAuthAuthorizationServerMetadata(t *testing.T) {
t.Run("ok", func(t *testing.T) {
Expand Down Expand Up @@ -98,8 +98,6 @@ func TestWrapper_OAuthAuthorizationServerMetadata(t *testing.T) {
}

func TestWrapper_GetWebDID(t *testing.T) {
webDID := did.MustParseDID("did:web:example.com:iam:123")
id := "123"
ctx := audit.TestContext()
expectedWebDIDDoc := did.Document{
ID: webDID,
Expand All @@ -112,7 +110,7 @@ func TestWrapper_GetWebDID(t *testing.T) {
test := newTestClient(t)
test.vdr.EXPECT().ResolveManaged(webDID).Return(&expectedWebDIDDoc, nil)

response, err := test.client.GetWebDID(ctx, GetWebDIDRequestObject{id})
response, err := test.client.GetWebDID(ctx, GetWebDIDRequestObject{webIDPart})

assert.NoError(t, err)
assert.Equal(t, expectedWebDIDDoc, did.Document(response.(GetWebDID200JSONResponse)))
Expand All @@ -121,7 +119,7 @@ func TestWrapper_GetWebDID(t *testing.T) {
test := newTestClient(t)
test.vdr.EXPECT().ResolveManaged(webDID).Return(nil, resolver.ErrNotFound)

response, err := test.client.GetWebDID(ctx, GetWebDIDRequestObject{id})
response, err := test.client.GetWebDID(ctx, GetWebDIDRequestObject{webIDPart})

assert.NoError(t, err)
assert.IsType(t, GetWebDID404Response{}, response)
Expand All @@ -130,7 +128,7 @@ func TestWrapper_GetWebDID(t *testing.T) {
test := newTestClient(t)
test.vdr.EXPECT().ResolveManaged(webDID).Return(nil, errors.New("failed"))

response, err := test.client.GetWebDID(ctx, GetWebDIDRequestObject{id})
response, err := test.client.GetWebDID(ctx, GetWebDIDRequestObject{webIDPart})

assert.EqualError(t, err, "unable to resolve DID")
assert.Nil(t, response)
Expand All @@ -140,27 +138,27 @@ func TestWrapper_GetWebDID(t *testing.T) {
func TestWrapper_GetOAuthClientMetadata(t *testing.T) {
t.Run("ok", func(t *testing.T) {
ctx := newTestClient(t)
ctx.vdr.EXPECT().IsOwner(nil, nutsDID).Return(true, nil)
ctx.vdr.EXPECT().IsOwner(nil, webDID).Return(true, nil)

res, err := ctx.client.OAuthClientMetadata(nil, OAuthClientMetadataRequestObject{Id: nutsDID.ID})
res, err := ctx.client.OAuthClientMetadata(nil, OAuthClientMetadataRequestObject{Id: webIDPart})

require.NoError(t, err)
assert.IsType(t, OAuthClientMetadata200JSONResponse{}, res)
})
t.Run("error - did not managed by this node", func(t *testing.T) {
ctx := newTestClient(t)
ctx.vdr.EXPECT().IsOwner(nil, nutsDID)
ctx.vdr.EXPECT().IsOwner(nil, webDID)

res, err := ctx.client.OAuthClientMetadata(nil, OAuthClientMetadataRequestObject{Id: nutsDID.ID})
res, err := ctx.client.OAuthClientMetadata(nil, OAuthClientMetadataRequestObject{Id: webIDPart})

assert.Equal(t, 404, statusCodeFrom(err))
assert.Nil(t, res)
})
t.Run("error - internal error 500", func(t *testing.T) {
ctx := newTestClient(t)
ctx.vdr.EXPECT().IsOwner(nil, nutsDID).Return(false, errors.New("unknown error"))
ctx.vdr.EXPECT().IsOwner(nil, webDID).Return(false, errors.New("unknown error"))

res, err := ctx.client.OAuthClientMetadata(nil, OAuthClientMetadataRequestObject{Id: nutsDID.ID})
res, err := ctx.client.OAuthClientMetadata(nil, OAuthClientMetadataRequestObject{Id: webIDPart})

assert.Equal(t, 500, statusCodeFrom(err))
assert.EqualError(t, err, "unknown error")
Expand Down Expand Up @@ -209,24 +207,55 @@ func TestWrapper_PresentationDefinition(t *testing.T) {
}

func TestWrapper_HandleAuthorizeRequest(t *testing.T) {
metadata := oauth.AuthorizationServerMetadata{
AuthorizationEndpoint: "https://example.com/holder/authorize",
}
t.Run("ok - from holder", func(t *testing.T) {
ctx := newTestClient(t)
ctx.vdr.EXPECT().IsOwner(gomock.Any(), verifierDID).Return(true, nil)
ctx.relyingParty.EXPECT().AuthorizationServerMetadata(gomock.Any(), holderDID).Return(&metadata, nil)

res, err := ctx.client.HandleAuthorizeRequest(requestContext(map[string]string{
clientIDParam: holderDID.String(),
redirectURIParam: "https://example.com",
responseTypeParam: "code",
scopeParam: "test",
}), HandleAuthorizeRequestRequestObject{
Id: "verifier",
})

require.NoError(t, err)
assert.IsType(t, HandleAuthorizeRequest302Response{}, res)
location := res.(HandleAuthorizeRequest302Response).Headers.Location
assert.Contains(t, location, "https://example.com/holder/authorize")
assert.Contains(t, location, "client_id=did%3Aweb%3Aexample.com%3Aiam%3Averifier")
assert.Contains(t, location, "nonce=")
assert.Contains(t, location, "presentation_definition_uri=https%3A%2F%2Fexample.com%2Fiam%2Fverifier%2Fpresentation_definition%3Fscope%3Dtest")
assert.Contains(t, location, "redirect_uri=https%3A%2F%2Fexample.com%2Fiam%2Fverifier%2Fresponse")
assert.Contains(t, location, "response_mode=direct_post")
assert.Contains(t, location, "response_type=vp_token")

})
t.Run("missing redirect_uri", func(t *testing.T) {
ctx := newTestClient(t)
ctx.vdr.EXPECT().IsOwner(gomock.Any(), webDID).Return(true, nil)

res, err := ctx.client.HandleAuthorizeRequest(requestContext(map[string]string{}), HandleAuthorizeRequestRequestObject{
Id: nutsDID.String(),
Id: webIDPart,
})

requireOAuthError(t, err, oauth.InvalidRequest, "redirect_uri is required")
assert.Nil(t, res)
})
t.Run("unsupported response type", func(t *testing.T) {
ctx := newTestClient(t)
ctx.vdr.EXPECT().IsOwner(gomock.Any(), webDID).Return(true, nil)

res, err := ctx.client.HandleAuthorizeRequest(requestContext(map[string]string{
"redirect_uri": "https://example.com",
"response_type": "unsupported",
}), HandleAuthorizeRequestRequestObject{
Id: nutsDID.String(),
Id: webIDPart,
})

requireOAuthError(t, err, oauth.UnsupportedResponseType, "")
Expand All @@ -239,7 +268,7 @@ func TestWrapper_HandleTokenRequest(t *testing.T) {
ctx := newTestClient(t)

res, err := ctx.client.HandleTokenRequest(nil, HandleTokenRequestRequestObject{
Id: nutsDID.String(),
Id: webDID.String(),
Body: &HandleTokenRequestFormdataRequestBody{
GrantType: "unsupported",
},
Expand Down
Loading

0 comments on commit 43b7105

Please sign in to comment.