diff --git a/auth/api/iam/api_test.go b/auth/api/iam/api_test.go index bff9a8c8c2..fd56e2a2c8 100644 --- a/auth/api/iam/api_test.go +++ b/auth/api/iam/api_test.go @@ -27,6 +27,7 @@ import ( "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/storage" "github.com/nuts-foundation/nuts-node/vdr" "github.com/nuts-foundation/nuts-node/vdr/resolver" "github.com/stretchr/testify/assert" @@ -241,18 +242,21 @@ func newTestClient(t testing.TB) *testCtx { publicURL, err := url.Parse("https://example.com") require.NoError(t, err) ctrl := gomock.NewController(t) + storageEngine := storage.NewTestStorageEngine(t) authnServices := auth.NewMockAuthenticationServices(ctrl) authnServices.EXPECT().PublicURL().Return(publicURL).AnyTimes() resolver := resolver.NewMockDIDResolver(ctrl) vdr := vdr.NewMockVDR(ctrl) vdr.EXPECT().Resolver().Return(resolver).AnyTimes() + return &testCtx{ authnServices: authnServices, resolver: resolver, vdr: vdr, client: &Wrapper{ - auth: authnServices, - vdr: vdr, + auth: authnServices, + vdr: vdr, + storageEngine: storageEngine, }, } } diff --git a/auth/api/iam/s2s_vptoken.go b/auth/api/iam/s2s_vptoken.go index 65fa221404..7c16066277 100644 --- a/auth/api/iam/s2s_vptoken.go +++ b/auth/api/iam/s2s_vptoken.go @@ -20,14 +20,27 @@ package iam import ( "context" + "crypto/rand" + "encoding/base64" "errors" + "fmt" "github.com/labstack/echo/v4" "github.com/nuts-foundation/go-did/did" + "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/core" + "github.com/nuts-foundation/nuts-node/storage" "github.com/nuts-foundation/nuts-node/vdr/resolver" "net/http" + "time" ) +// secretSizeBits is the size of the generated random secrets (access tokens, pre-authorized codes) in bits. +const secretSizeBits = 128 + +// accessTokenValidity defines how long access tokens are valid. +// TODO: Might want to make this configurable at some point +const accessTokenValidity = 15 * time.Minute + // serviceToService adds support for service-to-service OAuth2 flows, // which uses a custom vp_token grant to authenticate calls to the token endpoint. // Clients first call the presentation definition endpoint to get a presentation definition for the desired scope, @@ -99,3 +112,43 @@ func (r Wrapper) RequestAccessToken(ctx context.Context, request RequestAccessTo return RequestAccessToken200JSONResponse{}, nil } + +func (r Wrapper) createAccessToken(issuer did.DID, issueTime time.Time, presentation vc.VerifiablePresentation, scope string) (*TokenResponse, error) { + accessToken := AccessToken{ + Token: generateCode(), + Issuer: issuer.String(), + Expiration: issueTime.Add(accessTokenValidity), + Presentation: presentation, + } + err := r.accessTokenStore(issuer).Put(accessToken.Token, accessToken) + if err != nil { + return nil, fmt.Errorf("unable to store access token: %w", err) + } + expiresIn := int(accessTokenValidity.Seconds()) + return &TokenResponse{ + AccessToken: accessToken.Token, + ExpiresIn: &expiresIn, + Scope: &scope, + TokenType: "bearer", + }, nil +} + +func (r Wrapper) accessTokenStore(issuer did.DID) storage.SessionStore { + return r.storageEngine.GetSessionDatabase().GetStore(accessTokenValidity, "s2s", issuer.String(), "accesstoken") +} + +func generateCode() string { + buf := make([]byte, secretSizeBits/8) + _, err := rand.Read(buf) + if err != nil { + panic(err) + } + return base64.URLEncoding.EncodeToString(buf) +} + +type AccessToken struct { + Token string + Issuer string + Expiration time.Time + Presentation vc.VerifiablePresentation +} diff --git a/auth/api/iam/s2s_vptoken_test.go b/auth/api/iam/s2s_vptoken_test.go index e438585179..858aab47e4 100644 --- a/auth/api/iam/s2s_vptoken_test.go +++ b/auth/api/iam/s2s_vptoken_test.go @@ -19,12 +19,14 @@ package iam import ( - "github.com/nuts-foundation/nuts-node/vdr/resolver" - "testing" - "github.com/nuts-foundation/go-did/did" + "github.com/nuts-foundation/go-did/vc" + "github.com/nuts-foundation/nuts-node/jsonld" + "github.com/nuts-foundation/nuts-node/vdr/resolver" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "testing" + "time" ) func TestWrapper_RequestAccessToken(t *testing.T) { @@ -86,3 +88,32 @@ func TestWrapper_RequestAccessToken(t *testing.T) { assert.EqualError(t, err, "verifier not found: unable to find the DID document") }) } + +func TestWrapper_createAccessToken(t *testing.T) { + credential, err := vc.ParseVerifiableCredential(jsonld.TestOrganizationCredential) + require.NoError(t, err) + presentation := vc.VerifiablePresentation{ + VerifiableCredential: []vc.VerifiableCredential{*credential}, + } + t.Run("ok", func(t *testing.T) { + ctx := newTestClient(t) + + accessToken, err := ctx.client.createAccessToken(issuerDID, time.Now(), presentation, "everything") + + require.NoError(t, err) + assert.NotEmpty(t, accessToken.AccessToken) + assert.Equal(t, "bearer", accessToken.TokenType) + assert.Equal(t, 900, *accessToken.ExpiresIn) + assert.Equal(t, "everything", *accessToken.Scope) + + var storedToken AccessToken + err = ctx.client.accessTokenStore(issuerDID).Get(accessToken.AccessToken, &storedToken) + require.NoError(t, err) + assert.Equal(t, accessToken.AccessToken, storedToken.Token) + expectedVPJSON, _ := presentation.MarshalJSON() + actualVPJSON, _ := storedToken.Presentation.MarshalJSON() + assert.JSONEq(t, string(expectedVPJSON), string(actualVPJSON)) + assert.Equal(t, issuerDID.String(), storedToken.Issuer) + assert.NotEmpty(t, storedToken.Expiration) + }) +} diff --git a/storage/test.go b/storage/test.go index d1c6c07116..95fb052a18 100644 --- a/storage/test.go +++ b/storage/test.go @@ -34,7 +34,7 @@ func NewTestStorageEngineInDir(dir string) Engine { return result } -func NewTestStorageEngine(t *testing.T) Engine { +func NewTestStorageEngine(t testing.TB) Engine { oldOpts := append(DefaultBBoltOptions[:]) t.Cleanup(func() { DefaultBBoltOptions = oldOpts