Skip to content

Commit

Permalink
add introspection endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
gerardsn committed Oct 26, 2023
1 parent c5afa09 commit b7d76fa
Show file tree
Hide file tree
Showing 8 changed files with 436 additions and 10 deletions.
55 changes: 55 additions & 0 deletions auth/api/iam/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"html/template"
"net/http"
"strings"
"time"
)

var _ core.Routable = &Wrapper{}
Expand Down Expand Up @@ -143,6 +144,60 @@ func (r Wrapper) HandleTokenRequest(ctx context.Context, request HandleTokenRequ
}
}

// 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) {
// Client authentication if required, return 401 if fails.
// TODO: Is this relevant? Should be handled by API authentication middleware
clientAuthorized := true
if !clientAuthorized {
return IntrospectAccessToken401Response{}, nil
}

// Validate token
if request.Body.Token == "" {
// Return 200 + 'Active = false' when token is invalid or malformed
return IntrospectAccessToken200JSONResponse{}, nil
}

token := AccessToken{}
if err := r.s2sAccessTokenStore().Get(request.Body.Token, &token); err != nil {
// Return 200 + 'Active = false' when token is invalid or malformed
return IntrospectAccessToken200JSONResponse{}, err
}

if token.Expiration.Before(time.Now()) {
// Return 200 + 'Active = false' when token is invalid or malformed
// can happen between token expiration and pruning of database
return IntrospectAccessToken200JSONResponse{}, nil
}

// Create and return introspection response
iat := int(token.IssuedAt.Unix())
exp := int(token.Expiration.Unix())
response := IntrospectAccessToken200JSONResponse{
Active: true,
Iat: &iat,
Exp: &exp,
Iss: &token.Issuer,
Sub: &token.Issuer,
ClientId: &token.ClientId,
// TODO: include what resources the token is valid for
Aud: nil,
Scope: &token.Scope,
Vcs: &token.Presentation.VerifiableCredential,

// TODO: user authentication, used in OpenID4VP flow
FamilyName: nil,
Prefix: nil,
Initials: nil,
AssuranceLevel: nil,
Email: nil,
UserRole: nil,
Username: nil,
}
return response, nil
}

// HandleAuthorizeRequest handles calls to the authorization endpoint for starting an authorization code flow.
func (r Wrapper) HandleAuthorizeRequest(ctx context.Context, request HandleAuthorizeRequestRequestObject) (HandleAuthorizeRequestResponseObject, error) {
ownDID := idToDID(request.Id)
Expand Down
84 changes: 84 additions & 0 deletions auth/api/iam/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@ package iam

import (
"context"
"encoding/json"
"errors"
"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/audit"
"github.com/nuts-foundation/nuts-node/auth"
"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/jsonld"
"github.com/nuts-foundation/nuts-node/storage"
"github.com/nuts-foundation/nuts-node/vdr"
"github.com/nuts-foundation/nuts-node/vdr/resolver"
Expand All @@ -37,6 +40,7 @@ import (
"net/http/httptest"
"net/url"
"testing"
"time"
)

var nutsDID = did.MustParseDID("did:nuts:123")
Expand Down Expand Up @@ -202,6 +206,86 @@ func TestWrapper_HandleTokenRequest(t *testing.T) {
})
}

func TestWrapper_IntrospectAccessToken(t *testing.T) {
// mvp to store access token
ctx := newTestClient(t)

// validate all fields are there after introspection
t.Run("error - no token provided", func(t *testing.T) {
res, err := ctx.client.IntrospectAccessToken(context.Background(), IntrospectAccessTokenRequestObject{Body: &TokenIntrospectionRequest{Token: ""}})
require.NoError(t, err)
assert.Equal(t, res, IntrospectAccessToken200JSONResponse{})
})
t.Run("error - does not exist", func(t *testing.T) {
res, err := ctx.client.IntrospectAccessToken(context.Background(), IntrospectAccessTokenRequestObject{Body: &TokenIntrospectionRequest{Token: "does not exist"}})
require.ErrorIs(t, err, storage.ErrNotFound)
assert.Equal(t, res, IntrospectAccessToken200JSONResponse{})
})
t.Run("error - expired token", func(t *testing.T) {
token := AccessToken{Expiration: time.Now().Add(-time.Second)}
require.NoError(t, ctx.client.s2sAccessTokenStore().Put("token", token))

res, err := ctx.client.IntrospectAccessToken(context.Background(), IntrospectAccessTokenRequestObject{Body: &TokenIntrospectionRequest{Token: "token"}})

require.NoError(t, err)
assert.Equal(t, res, IntrospectAccessToken200JSONResponse{})
})
t.Run("ok", func(t *testing.T) {
token := AccessToken{Expiration: time.Now().Add(time.Second)}
require.NoError(t, ctx.client.s2sAccessTokenStore().Put("token", token))

res, err := ctx.client.IntrospectAccessToken(context.Background(), IntrospectAccessTokenRequestObject{Body: &TokenIntrospectionRequest{Token: "token"}})

require.NoError(t, err)
tokenResponse, ok := res.(IntrospectAccessToken200JSONResponse)
require.True(t, ok)
assert.True(t, tokenResponse.Active)
})

t.Run(" ok - s2s flow", func(t *testing.T) {
// TODO: this should be an integration test to make sure all fields are set
credential, err := vc.ParseVerifiableCredential(jsonld.TestOrganizationCredential)
require.NoError(t, err)
presentation := vc.VerifiablePresentation{
VerifiableCredential: []vc.VerifiableCredential{*credential},
}
tNow := time.Now()
token := AccessToken{
Token: "token",
Issuer: "resource-owner",
ClientId: "client",
IssuedAt: tNow,
Expiration: tNow.Add(time.Minute),
Scope: "test",
Presentation: presentation,
}
pStr := func(s string) *string { return &s }
pTime := func(t time.Time) *int {
tInt := int(t.Unix())
return &tInt
}
require.NoError(t, ctx.client.s2sAccessTokenStore().Put(token.Token, token))
expectedResponse, err := json.Marshal(IntrospectAccessToken200JSONResponse{
Active: true,
ClientId: pStr("client"),
Exp: pTime(tNow.Add(time.Minute)),
Iat: pTime(tNow),
Iss: pStr("resource-owner"),
Scope: pStr("test"),
Sub: pStr("resource-owner"),
Vcs: &[]vc.VerifiableCredential{*credential},
})
require.NoError(t, err)

res, err := ctx.client.IntrospectAccessToken(context.Background(), IntrospectAccessTokenRequestObject{Body: &TokenIntrospectionRequest{Token: "token"}})

require.NoError(t, err)
tokenResponse, err := json.Marshal(res)
assert.NoError(t, err)
assert.JSONEq(t, string(expectedResponse), string(tokenResponse))
})
}

func requireOAuthError(t *testing.T, err error, errorCode ErrorCode, errorDescription string) {
var oauthErr OAuth2Error
require.ErrorAs(t, err, &oauthErr)
Expand Down
Loading

0 comments on commit b7d76fa

Please sign in to comment.