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
  • Loading branch information
woutslakhorst committed Dec 6, 2023
1 parent 03e0eae commit f32b958
Show file tree
Hide file tree
Showing 14 changed files with 387 additions and 294 deletions.
90 changes: 52 additions & 38 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,15 +325,9 @@ 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)
owned, err := r.vdr.IsOwner(ctx, ownDID)
_, err := r.idToOwnedDID(ctx, request.Id)
if err != nil {
log.Logger().WithField("did", ownDID.String()).Errorf("oauth metadata: failed to assert ownership of did: %s", err.Error())
return nil, core.Error(500, err.Error())
}
if !owned {
return nil, core.NotFoundError("did not owned")
return nil, err
}

identity := r.auth.PublicURL().JoinPath("iam", request.Id)
Expand Down Expand Up @@ -365,17 +367,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
}
63 changes: 46 additions & 17 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 @@ -92,14 +92,12 @@ func TestWrapper_OAuthAuthorizationServerMetadata(t *testing.T) {
res, err := ctx.client.OAuthAuthorizationServerMetadata(nil, OAuthAuthorizationServerMetadataRequestObject{Id: webIDPart})

assert.Equal(t, 500, statusCodeFrom(err))
assert.EqualError(t, err, "authz server metadata: unknown error")
assert.EqualError(t, err, "server_error - failed to assert ownership of did")
assert.Nil(t, res)
})
}

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,30 +138,30 @@ 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")
assert.EqualError(t, err, "server_error - failed to assert ownership of did")
assert.Nil(t, res)
})
}
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 f32b958

Please sign in to comment.