Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 31 additions & 25 deletions auth/api/iam/access_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ package iam

import (
"fmt"
"reflect"
"time"

"github.com/nuts-foundation/nuts-node/auth/oauth"
"github.com/nuts-foundation/nuts-node/core/to"
"github.com/nuts-foundation/nuts-node/crypto"
"time"

"github.com/nuts-foundation/nuts-node/crypto/dpop"
"github.com/nuts-foundation/nuts-node/vcr/pe"
Expand Down Expand Up @@ -59,34 +61,38 @@ type AccessToken struct {
PresentationDefinitions pe.WalletOwnerMapping `json:"presentation_definitions,omitempty"`
}

// createAccessToken is used in both the s2s and openid4vp flows
func (r Wrapper) createAccessToken(issuerURL string, clientID string, issueTime time.Time, scope string, pexState PEXConsumer, dpopToken *dpop.DPoP) (*oauth.TokenResponse, error) {
credentialMap, err := pexState.credentialMap()
if err != nil {
return nil, err
// AddInputDescriptorConstraintIdMap adds the given map to the access token.
// If there are already values in the map, they MUST equal the new values, otherwise an error is returned.
// This is used for having claims from multiple access policies/presentation definitions in the same access token,
// while preventing conflicts between them (2 policies specifying the same credential ID field for different credentials).
func (a *AccessToken) AddInputDescriptorConstraintIdMap(claims map[string]any) error {
if a.InputDescriptorConstraintIdMap == nil {
a.InputDescriptorConstraintIdMap = make(map[string]any)
}
fieldsMap, err := resolveInputDescriptorValues(pexState.RequiredPresentationDefinitions, credentialMap)
if err != nil {
return nil, err
for k, v := range claims {
if existing, ok := a.InputDescriptorConstraintIdMap[k]; ok {
if !reflect.DeepEqual(existing, v) {
return fmt.Errorf("conflicting values for input descriptor constraint id %s: existing value %v, new value %v", k, existing, v)
}
} else {
a.InputDescriptorConstraintIdMap[k] = v
}
}
return nil
}

accessToken := AccessToken{
DPoP: dpopToken,
Token: crypto.GenerateNonce(),
Issuer: issuerURL,
IssuedAt: issueTime,
ClientId: clientID,
Expiration: issueTime.Add(accessTokenValidity),
Scope: scope,
PresentationSubmissions: pexState.Submissions,
PresentationDefinitions: pexState.RequiredPresentationDefinitions,
InputDescriptorConstraintIdMap: fieldsMap,
}
for _, envelope := range pexState.SubmittedEnvelopes {
accessToken.VPToken = append(accessToken.VPToken, envelope.Presentations...)
}
// createAccessToken is used in both the s2s and openid4vp flows
func (r Wrapper) createAccessToken(issuerURL string, clientID string, issueTime time.Time, scope string, template AccessToken, dpopToken *dpop.DPoP) (*oauth.TokenResponse, error) {
accessToken := template
accessToken.DPoP = dpopToken
accessToken.Token = crypto.GenerateNonce()
accessToken.Issuer = issuerURL
accessToken.IssuedAt = issueTime
accessToken.ClientId = clientID
accessToken.Expiration = issueTime.Add(accessTokenValidity)
accessToken.Scope = scope

err = r.accessTokenServerStore().Put(accessToken.Token, accessToken)
err := r.accessTokenServerStore().Put(accessToken.Token, accessToken)
if err != nil {
return nil, fmt.Errorf("unable to store access token: %w", err)
}
Expand Down
47 changes: 33 additions & 14 deletions auth/api/iam/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/nuts-foundation/nuts-node/core/to"
"html/template"
"net/http"
"net/url"
"slices"
"strings"
"time"

"github.com/nuts-foundation/nuts-node/core/to"

"github.com/labstack/echo/v4"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jwt"
Expand Down Expand Up @@ -225,7 +226,7 @@ func (r Wrapper) HandleTokenRequest(ctx context.Context, request HandleTokenRequ
// - OpenID4VCI
// - OpenID4VP
// verifier DID is taken from code->oauthSession storage
return r.handleAccessTokenRequest(ctx, *request.Body)
return r.handleAuthzCodeTokenRequest(ctx, *request.Body)
case oauth.PreAuthorizedCodeGrantType:
// Options:
// - OpenID4VCI
Expand All @@ -234,6 +235,16 @@ func (r Wrapper) HandleTokenRequest(ctx context.Context, request HandleTokenRequ
Code: oauth.UnsupportedGrantType,
Description: "not implemented yet",
}
case oauth.JWTBearerGrantType:
// NL Generic Functions Authentication flow
if request.Body.Assertion == nil || request.Body.Scope == nil ||
request.Body.ClientId == nil || request.Body.ClientAssertion == nil {
return nil, oauth.OAuth2Error{
Code: oauth.InvalidRequest,
Description: "missing required parameters",
}
}
return r.handleJWTBearerTokenRequest(ctx, *request.Body.ClientId, request.SubjectID, *request.Body.Scope, *request.Body.ClientAssertion, *request.Body.Assertion)
case oauth.VpTokenGrantType:
// Nuts RFC021 vp_token bearer flow
if request.Body.PresentationSubmission == nil || request.Body.Scope == nil || request.Body.Assertion == nil || request.Body.ClientId == nil {
Expand All @@ -242,7 +253,7 @@ func (r Wrapper) HandleTokenRequest(ctx context.Context, request HandleTokenRequ
Description: "missing required parameters",
}
}
return r.handleS2SAccessTokenRequest(ctx, *request.Body.ClientId, request.SubjectID, *request.Body.Scope, *request.Body.PresentationSubmission, *request.Body.Assertion)
return r.handleRFC021VPTokenRequest(ctx, *request.Body.ClientId, request.SubjectID, *request.Body.Scope, *request.Body.PresentationSubmission, *request.Body.Assertion)
default:
return nil, oauth.OAuth2Error{
Code: oauth.UnsupportedGrantType,
Expand Down Expand Up @@ -419,16 +430,20 @@ func (r Wrapper) introspectAccessToken(input string) (*ExtendedTokenIntrospectio
iat := int(token.IssuedAt.Unix())
exp := int(token.Expiration.Unix())
response := ExtendedTokenIntrospectionResponse{
Active: true,
Cnf: cnf,
Iat: &iat,
Exp: &exp,
Iss: &token.Issuer,
ClientId: &token.ClientId,
Scope: &token.Scope,
Vps: &token.VPToken,
PresentationDefinitions: &token.PresentationDefinitions,
PresentationSubmissions: &token.PresentationSubmissions,
Active: true,
Cnf: cnf,
Iat: &iat,
Exp: &exp,
Iss: &token.Issuer,
ClientId: &token.ClientId,
Scope: &token.Scope,
Vps: &token.VPToken,
}
if token.PresentationDefinitions != nil {
response.PresentationDefinitions = &token.PresentationDefinitions
}
if token.PresentationSubmissions != nil {
response.PresentationSubmissions = &token.PresentationSubmissions
}

if token.InputDescriptorConstraintIdMap != nil {
Expand Down Expand Up @@ -774,7 +789,11 @@ func (r Wrapper) RequestServiceAccessToken(ctx context.Context, request RequestS
useDPoP = false
}
clientID := r.subjectToBaseURL(request.SubjectID)
tokenResult, err := r.auth.IAMClient().RequestRFC021AccessToken(ctx, clientID.String(), request.SubjectID, request.Body.AuthorizationServer, request.Body.Scope, useDPoP, credentials)
var policyId string
if request.Body.PolicyId != nil {
policyId = *request.Body.PolicyId
}
tokenResult, err := r.auth.IAMClient().RequestRFC021AccessToken(ctx, clientID.String(), request.SubjectID, request.Body.AuthorizationServer, request.Body.Scope, policyId, useDPoP, credentials)
if err != nil {
// this can be an internal server error, a 400 oauth error or a 412 precondition failed if the wallet does not contain the required credentials
return nil, err
Expand Down
18 changes: 9 additions & 9 deletions auth/api/iam/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -886,7 +886,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) {
request := RequestServiceAccessTokenRequestObject{SubjectID: holderSubjectID, Body: body}
request.Params.CacheControl = to.Ptr("no-cache")
// Initial call to populate cache
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil).Return(response, nil).Times(2)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", "", true, nil).Return(response, nil).Times(2)
token, err := ctx.client.RequestServiceAccessToken(nil, request)

// Test call to check cache is bypassed
Expand All @@ -907,7 +907,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) {
TokenType: "Bearer",
ExpiresIn: to.Ptr(900),
}
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil).Return(response, nil)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", "", true, nil).Return(response, nil)

token, err := ctx.client.RequestServiceAccessToken(nil, request)

Expand Down Expand Up @@ -946,7 +946,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) {
t.Run("cache expired", func(t *testing.T) {
cacheKey := accessTokenRequestCacheKey(request)
_ = ctx.client.accessTokenCache().Delete(cacheKey)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil).Return(&oauth.TokenResponse{AccessToken: "other"}, nil)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", "", true, nil).Return(&oauth.TokenResponse{AccessToken: "other"}, nil)

otherToken, err := ctx.client.RequestServiceAccessToken(nil, request)

Expand All @@ -963,7 +963,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) {
Scope: "first second",
TokenType: &tokenTypeBearer,
}
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", false, nil).Return(&oauth.TokenResponse{}, nil)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", "", false, nil).Return(&oauth.TokenResponse{}, nil)

_, err := ctx.client.RequestServiceAccessToken(nil, RequestServiceAccessTokenRequestObject{SubjectID: holderSubjectID, Body: body})

Expand All @@ -972,7 +972,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) {
t.Run("ok with expired cache by ttl", func(t *testing.T) {
ctx := newTestClient(t)
request := RequestServiceAccessTokenRequestObject{SubjectID: holderSubjectID, Body: body}
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil).Return(&oauth.TokenResponse{ExpiresIn: to.Ptr(5)}, nil)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", "", true, nil).Return(&oauth.TokenResponse{ExpiresIn: to.Ptr(5)}, nil)

_, err := ctx.client.RequestServiceAccessToken(nil, request)

Expand All @@ -981,7 +981,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) {
})
t.Run("error - no matching credentials", func(t *testing.T) {
ctx := newTestClient(t)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil).Return(nil, pe.ErrNoCredentials)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", "", true, nil).Return(nil, pe.ErrNoCredentials)

_, err := ctx.client.RequestServiceAccessToken(nil, RequestServiceAccessTokenRequestObject{SubjectID: holderSubjectID, Body: body})

Expand All @@ -997,8 +997,8 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) {
ctx.client.storageEngine = mockStorage

request := RequestServiceAccessTokenRequestObject{SubjectID: holderSubjectID, Body: body}
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil).Return(&oauth.TokenResponse{AccessToken: "first"}, nil)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil).Return(&oauth.TokenResponse{AccessToken: "second"}, nil)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", "", true, nil).Return(&oauth.TokenResponse{AccessToken: "first"}, nil)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", "", true, nil).Return(&oauth.TokenResponse{AccessToken: "second"}, nil)

token1, err := ctx.client.RequestServiceAccessToken(nil, request)
require.NoError(t, err)
Expand All @@ -1023,7 +1023,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) {
{ID: to.Ptr(ssi.MustParseURI("not empty"))},
}
request := RequestServiceAccessTokenRequestObject{SubjectID: holderSubjectID, Body: body}
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, *body.Credentials).Return(response, nil)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", "", true, *body.Credentials).Return(response, nil)

_, err := ctx.client.RequestServiceAccessToken(nil, request)

Expand Down
Loading
Loading