From d43bbcb36826f4180d5fe4b1f60625f6b291e06e Mon Sep 17 00:00:00 2001 From: Stefan Wiedemann Date: Fri, 2 May 2025 15:57:16 +0200 Subject: [PATCH 1/3] presentation definition and request modes --- api/api.yaml | 36 +++- common/fileSystem.go | 14 ++ common/tokenSigner.go | 9 +- config/config.go | 15 ++ config/configClient.go | 64 +++++- config/configClient_test.go | 30 ++- config/data/ccs_full.json | 50 +++-- config/data/config_test.yaml | 22 ++- config/provider_test.go | 31 ++- go.mod | 36 ++-- go.sum | 65 +++---- openapi/api_api.go | 31 ++- openapi/api_api_test.go | 13 +- openapi/api_frontend.go | 10 +- openapi/routers.go | 7 + tir/tokenProvider.go | 14 +- verifier/credentialsConfig.go | 29 ++- verifier/verifier.go | 353 ++++++++++++++++++++++++++++++---- verifier/verifier_test.go | 289 +++++++++++++++++++--------- 19 files changed, 870 insertions(+), 248 deletions(-) create mode 100644 common/fileSystem.go diff --git a/api/api.yaml b/api/api.yaml index 101482d5..af586080 100644 --- a/api/api.yaml +++ b/api/api.yaml @@ -18,6 +18,7 @@ paths: - $ref: '#/components/parameters/QueryState' - $ref: '#/components/parameters/ClientCallback' - $ref: '#/components/parameters/ClientId' + - $ref: '#/components/parameters/RequestMode' operationId: VerifierPageDisplayQRSIOP summary: Presents a qr as starting point for the auth process description: Returns a rendered html with a QR encoding the login-starting point for the siop flow - e.g. 'openid://?scope=somethign&response_type=rt&response_mode=rm&client_id=ci&redirect_uri=uri&state=state&nonce=nonce' @@ -34,7 +35,30 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorMessage' - + + /api/v1/request/{id}: + get: + tags: + - api + parameters: + - $ref: '#/components/parameters/Id' + operationId: GetRequestByReference + summary: Get the request object by reference + description: Returns the request object by reference as defined in https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#section-5 + responses: + '200': + description: The jwt encoded request object + content: + text/html: + schema: + type: string + '400': + description: In case of broken requests. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + /api/v1/samedevice: get: tags: @@ -247,6 +271,16 @@ components: schema: type: string example: packet-delivery-portal + RequestMode: + name: request_mode + description: Mode to be used for the authorization request. + in: query + required: false + schema: + type: string + enum: + - urlEncoded + - byValue ServiceId: name: service_id description: The id of the client/service that intents to start the authentication flow. Will be used to retrieve the scope and trust services to be used for verification. diff --git a/common/fileSystem.go b/common/fileSystem.go new file mode 100644 index 00000000..e1ec59bb --- /dev/null +++ b/common/fileSystem.go @@ -0,0 +1,14 @@ +package common + +import "os" + +// file system interfaces + +type FileAccessor interface { + ReadFile(filename string) ([]byte, error) +} +type DiskFileAccessor struct{} + +func (DiskFileAccessor) ReadFile(filename string) ([]byte, error) { + return os.ReadFile(filename) +} diff --git a/common/tokenSigner.go b/common/tokenSigner.go index 35f302a1..891abe0d 100644 --- a/common/tokenSigner.go +++ b/common/tokenSigner.go @@ -1,16 +1,15 @@ package common import ( - "github.com/lestrrat-go/jwx/jwa" - "github.com/lestrrat-go/jwx/jwt" + "github.com/lestrrat-go/jwx/v3/jwt" ) type TokenSigner interface { - Sign(t jwt.Token, alg jwa.SignatureAlgorithm, key interface{}, options ...jwt.SignOption) ([]byte, error) + Sign(t jwt.Token, options ...jwt.SignOption) ([]byte, error) } type JwtTokenSigner struct{} -func (JwtTokenSigner) Sign(t jwt.Token, alg jwa.SignatureAlgorithm, key interface{}, options ...jwt.SignOption) ([]byte, error) { - return jwt.Sign(t, alg, key, options...) +func (JwtTokenSigner) Sign(t jwt.Token, options ...jwt.SignOption) ([]byte, error) { + return jwt.Sign(t, options...) } diff --git a/config/config.go b/config/config.go index 1681f01c..2e00a426 100644 --- a/config/config.go +++ b/config/config.go @@ -58,6 +58,10 @@ type Logging struct { type Verifier struct { // did to be used by the verifier Did string `mapstructure:"did"` + // Identification to be used for the verifier + ClientIdentification ClientIdentification `mapstructure:"clientIdentification"` + // supported request modes - currently 'urlEncoded', 'byValue' an 'byReference' are available. In case of byValue, the keyPath has to be set. + SupportedModes []string `mapstructure:"supportedModes" default:"urlEncoded"` // address of the (ebsi-compatible) trusted-issuers-registry for verifying the issuer TirAddress string `mapstructure:"tirAddress"` // expiry of the tir-cache entries @@ -80,6 +84,17 @@ type Verifier struct { KeyAlgorithm string `mapstructure:"keyAlgorithm" default:"RS256"` } +type ClientIdentification struct { + // path to the did signing key(in pem format) for request object mode + KeyPath string `mapstructure:"keyPath"` + // algorithm used for the request signing key + KeyAlgorithm string `mapstructure:"requestKeyAlgorithm"` + // identification used by the verifier when requesting authorization. Can be a did, but also methods like x509_san_dns + Id string `mapstructure:"id"` + // optional path to the certifcate to embed in the jwt header + CertificatePath string `mapstructure:"certificatePath"` +} + type Elsi struct { // should the support for did:elsi be enabled Enabled bool `mapstructure:"enabled" default:"false"` diff --git a/config/configClient.go b/config/configClient.go index 948f7acd..6cf3a432 100644 --- a/config/configClient.go +++ b/config/configClient.go @@ -40,9 +40,16 @@ type ServicesResponse struct { type ConfiguredService struct { // Default OIDC scope to be used if none is specified - DefaultOidcScope string `json:"defaultOidcScope" mapstructure:"defaultOidcScope"` - ServiceScopes map[string][]Credential `json:"oidcScopes" mapstructure:"oidcScopes"` - Id string `json:"id" mapstructure:"id"` + DefaultOidcScope string `json:"defaultOidcScope" mapstructure:"defaultOidcScope"` + ServiceScopes map[string]ScopeEntry `json:"oidcScopes" mapstructure:"oidcScopes"` + Id string `json:"id" mapstructure:"id"` +} + +type ScopeEntry struct { + // credential types with their trust configuration + Credentials []Credential `json:"credentials" mapstructure:"credentials"` + // Proofs to be requested - see https://identity.foundation/presentation-exchange/#presentation-definition + PresentationDefinition PresentationDefinition `json:"presentationDefinition" mapstructure:"presentationDefinition"` } type Credential struct { @@ -70,6 +77,45 @@ type HolderVerification struct { Claim string `json:"claim" mapstructure:"claim"` } +type PresentationDefinition struct { + // Id of the definition + Id string `json:"id" mapstructure:"id"` + // List of requested inputs + InputDescriptors []InputDescriptor `json:"input_descriptors" mapstructure:"input_descriptors"` + // Format of the credential to be requested + Format map[string]FormatObject `json:"format" mapstructure:"format"` +} + +type FormatObject struct { + // list of algorithms to be requested for credential - f.e. ES256 + Alg []string `json:"alg" mapstructure:"alg"` +} + +type InputDescriptor struct { + // Id of the descriptor + Id string `json:"id" mapstructure:"id"` + // defines the infromation to be requested + Constraints Constraints `json:"constraints" mapstructure:"constraints"` + // Format of the credential to be requested + Format map[string]FormatObject `json:"format" mapstructure:"format"` +} + +type Constraints struct { + // array of objects to describe the information to be included + Fields []Fields `json:"fields" mapstructure:"fields"` +} + +type Fields struct { + // Id of the field + Id string `json:"id" mapstructure:"id"` + // A list of JsonPaths for the requested claim + Path []string `json:"path" mapstructure:"path"` + // Does it need to be included? + Optional bool `json:"optional" mapstructure:"optional" default:"true"` + // a custom filter to be applied on the fields, f.e. restrict to certain values + Filter interface{} `json:"filter" mapstructure:"filter"` +} + func (cs ConfiguredService) GetRequiredCredentialTypes(scope string) []string { types := []string{} for _, credential := range cs.GetCredentials(scope) { @@ -78,13 +124,23 @@ func (cs ConfiguredService) GetRequiredCredentialTypes(scope string) []string { return types } -func (cs ConfiguredService) GetCredentials(scope string) []Credential { +func (cs ConfiguredService) GetScope(scope string) ScopeEntry { if scope != SERVICE_DEFAULT_SCOPE { return cs.ServiceScopes[scope] } return cs.ServiceScopes[cs.DefaultOidcScope] } +func (cs ConfiguredService) GetCredentials(scope string) []Credential { + return cs.GetScope(scope).Credentials +} + +func (cs ConfiguredService) GetPresentationDefinition(scope string) PresentationDefinition { + logging.Log().Warnf("The ScopeEntry %s", logging.PrettyPrintObject(cs.GetScope(scope))) + + return cs.GetScope(scope).PresentationDefinition +} + func (cs ConfiguredService) GetCredential(scope, credentialType string) (Credential, bool) { credentials := cs.GetCredentials(scope) for _, credential := range credentials { diff --git a/config/configClient_test.go b/config/configClient_test.go index 18200a13..14a0edf8 100644 --- a/config/configClient_test.go +++ b/config/configClient_test.go @@ -39,13 +39,31 @@ func Test_getServices(t *testing.T) { { Id: "service_all", DefaultOidcScope: "did_write", - ServiceScopes: map[string][]Credential{ + ServiceScopes: map[string]ScopeEntry{ "did_write": { - { - Type: "VerifiableCredential", - TrustedParticipantsLists: []TrustedParticipantsList{{Type: "ebsi", Url: "https://tir-pdc.ebsi.fiware.dev"}}, - TrustedIssuersLists: []string{"https://til-pdc.ebsi.fiware.dev"}, - HolderVerification: HolderVerification{Enabled: false, Claim: "subject"}, + Credentials: []Credential{ + { + Type: "VerifiableCredential", + TrustedParticipantsLists: []TrustedParticipantsList{{Type: "ebsi", Url: "https://tir-pdc.ebsi.fiware.dev"}}, + TrustedIssuersLists: []string{"https://til-pdc.ebsi.fiware.dev"}, + HolderVerification: HolderVerification{Enabled: false, Claim: "subject"}, + }, + }, + PresentationDefinition: PresentationDefinition{ + Id: "my-pd", + InputDescriptors: []InputDescriptor{ + { + Id: "my-descriptor", + Constraints: Constraints{ + Fields: []Fields{ + { + Id: "my-field", + Path: []string{"$.vc.my.claim"}, + }, + }, + }, + }, + }, }, }, }, diff --git a/config/data/ccs_full.json b/config/data/ccs_full.json index fd3073ed..82fd4843 100644 --- a/config/data/ccs_full.json +++ b/config/data/ccs_full.json @@ -7,24 +7,44 @@ "id": "service_all", "defaultOidcScope": "did_write", "oidcScopes": { - "did_write": [ - { - "type": "VerifiableCredential", - "trustedParticipantsLists": [ - { - "type": "ebsi", - "url": "https://tir-pdc.ebsi.fiware.dev" + "did_write": { + "credentials": [ + { + "type": "VerifiableCredential", + "trustedParticipantsLists": [ + { + "type": "ebsi", + "url": "https://tir-pdc.ebsi.fiware.dev" + } + ], + "trustedIssuersLists": [ + "https://til-pdc.ebsi.fiware.dev" + ], + "holderVerification": { + "enabled": false, + "claim": "subject" } - ], - "trustedIssuersLists": [ - "https://til-pdc.ebsi.fiware.dev" - ], - "holderVerification": { - "enabled": false, - "claim": "subject" } + ], + "presentationDefinition": { + "id": "my-pd", + "input_descriptors": [ + { + "id": "my-descriptor", + "constraints": { + "fields": [ + { + "id": "my-field", + "path": [ + "$.vc.my.claim" + ] + } + ] + } + } + ] } - ] + } } } ] diff --git a/config/data/config_test.yaml b/config/data/config_test.yaml index df007047..3e01fd94 100644 --- a/config/data/config_test.yaml +++ b/config/data/config_test.yaml @@ -33,10 +33,20 @@ configRepo: defaultOidcScope: someScope oidcScopes: someScope: - - type: VerifiableCredential - trustedParticipantsLists: - - type: ebsi - url: https://tir-pdc.ebsi.fiware.dev - trustedIssuersLists: - - https://til-pdc.ebsi.fiware.dev + credentials: + - type: VerifiableCredential + trustedParticipantsLists: + - type: ebsi + url: https://tir-pdc.ebsi.fiware.dev + trustedIssuersLists: + - https://til-pdc.ebsi.fiware.dev + presentationDefinition: + id: my-pd + input_descriptors: + - id: my-descriptor + constraints: + fields: + - id: my-field + path: + - $.vc.my.claim diff --git a/config/provider_test.go b/config/provider_test.go index a0b62ff0..599ac7ff 100644 --- a/config/provider_test.go +++ b/config/provider_test.go @@ -47,6 +47,7 @@ func Test_ReadConfig(t *testing.T) { }, ValidationMode: "none", KeyAlgorithm: "RS256", + SupportedModes: []string{"urlEncoded"}, }, Logging: Logging{ Level: "DEBUG", @@ -60,12 +61,31 @@ func Test_ReadConfig(t *testing.T) { { Id: "testService", DefaultOidcScope: "someScope", - ServiceScopes: map[string][]Credential{ + ServiceScopes: map[string]ScopeEntry{ "someScope": { - { - Type: "VerifiableCredential", - TrustedParticipantsLists: []TrustedParticipantsList{{Type: "ebsi", Url: "https://tir-pdc.ebsi.fiware.dev"}}, - TrustedIssuersLists: []string{"https://til-pdc.ebsi.fiware.dev"}, + Credentials: []Credential{ + + { + Type: "VerifiableCredential", + TrustedParticipantsLists: []TrustedParticipantsList{{Type: "ebsi", Url: "https://tir-pdc.ebsi.fiware.dev"}}, + TrustedIssuersLists: []string{"https://til-pdc.ebsi.fiware.dev"}, + }, + }, + PresentationDefinition: PresentationDefinition{ + Id: "my-pd", + InputDescriptors: []InputDescriptor{ + { + Id: "my-descriptor", + Constraints: Constraints{ + Fields: []Fields{ + { + Id: "my-field", + Path: []string{"$.vc.my.claim"}, + }, + }, + }, + }, + }, }, }, }, @@ -91,6 +111,7 @@ func Test_ReadConfig(t *testing.T) { SessionExpiry: 30, ValidationMode: "none", KeyAlgorithm: "RS256", + SupportedModes: []string{"urlEncoded"}, }, Logging: Logging{ Level: "INFO", diff --git a/go.mod b/go.mod index ad06f752..dbe66b00 100644 --- a/go.mod +++ b/go.mod @@ -1,21 +1,23 @@ module github.com/fiware/VCVerifier -go 1.23 +go 1.23.0 + +toolchain go1.24.2 require ( github.com/deepmap/oapi-codegen v1.12.3 github.com/foolin/goview v0.3.0 github.com/gin-contrib/cors v1.4.0 github.com/gin-gonic/gin v1.9.1 + github.com/go-jose/go-jose/v3 v3.0.1-0.20221117193127-916db76e8214 github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.3.0 github.com/gookit/config/v2 v2.2.1 github.com/hellofresh/health-go/v5 v5.0.0 - github.com/lestrrat-go/jwx v1.2.25 - github.com/mitchellh/mapstructure v1.5.0 + github.com/lestrrat-go/jwx/v3 v3.0.1 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/piprate/json-gold v0.5.1-0.20230111113000-6ddbe6e6f19f - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.10.0 github.com/trustbloc/did-go v1.1.0 github.com/trustbloc/kms-go v1.1.0 github.com/trustbloc/vc-go v1.1.0 @@ -34,10 +36,9 @@ require ( github.com/consensys/bavard v0.1.13 // indirect github.com/consensys/gnark-crypto v0.12.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/fatih/color v1.14.1 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect - github.com/go-jose/go-jose/v3 v3.0.1-0.20221117193127-916db76e8214 // indirect github.com/goccy/go-yaml v1.10.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect @@ -47,13 +48,13 @@ require ( github.com/hyperledger/fabric-amcl v0.0.0-20230602173724-9e02669dceb2 // indirect github.com/imdario/mergo v0.3.13 // indirect github.com/kilic/bls12-381 v0.1.1-0.20210503002446-7b7597926c69 // indirect - github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect - github.com/lestrrat-go/blackmagic v1.0.2 // indirect + github.com/lestrrat-go/blackmagic v1.0.3 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect - github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/httprc/v3 v3.0.0-beta2 // indirect github.com/lestrrat-go/option v1.0.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect @@ -66,6 +67,7 @@ require ( github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect + github.com/segmentio/asm v1.2.0 // indirect github.com/teserakt-io/golang-ed25519 v0.0.0-20210104091850-3888c087a4c8 // indirect github.com/tidwall/gjson v1.14.3 // indirect github.com/tidwall/match v1.1.1 // indirect @@ -80,8 +82,8 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opentelemetry.io/otel v1.10.0 // indirect go.opentelemetry.io/otel/trace v1.10.0 // indirect - golang.org/x/sync v0.1.0 // indirect - golang.org/x/term v0.15.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/term v0.31.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect rsc.io/tmplfunc v0.0.3 // indirect ) @@ -94,7 +96,7 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.14.1 // indirect - github.com/goccy/go-json v0.10.2 // indirect + github.com/goccy/go-json v0.10.3 // indirect github.com/golang-jwt/jwt/v5 v5.2.0 github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect @@ -111,10 +113,10 @@ require ( github.com/ugorji/go/codec v1.2.11 // indirect github.com/valyala/fasttemplate v1.2.2 golang.org/x/arch v0.5.0 // indirect - golang.org/x/crypto v0.16.0 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.15.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/crypto v0.37.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.24.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect -) \ No newline at end of file +) diff --git a/go.sum b/go.sum index 1f30708d..8c1f84f8 100644 --- a/go.sum +++ b/go.sum @@ -102,10 +102,8 @@ github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/deepmap/oapi-codegen v1.12.3 h1:+DDYKeIwlKChzHjhVtlISegatFevDDazBhtk/dnp4V4= github.com/deepmap/oapi-codegen v1.12.3/go.mod h1:ao2aFwsl/muMHbez870+KelJ1yusV01RznwAFFrVjDc= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= @@ -163,8 +161,8 @@ github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+j github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.10.0 h1:rBi+5HGuznOxx0JZ+60LDY85gc0dyIJCIMvsMJTKSKQ= github.com/goccy/go-yaml v1.10.0/go.mod h1:h/18Lr6oSQ3mvmqFoWmQ47KChOgpfHpTyIHl3yVmpiY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -294,19 +292,14 @@ github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgx github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= -github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= -github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= -github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= -github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= -github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/blackmagic v1.0.3 h1:94HXkVLxkZO9vJI/w2u1T0DAoprShFd13xtnSINtDWs= +github.com/lestrrat-go/blackmagic v1.0.3/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= -github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= -github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= -github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx v1.2.25 h1:tAx93jN2SdPvFn08fHNAhqFJazn5mBBOB8Zli0g0otA= -github.com/lestrrat-go/jwx v1.2.25/go.mod h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY= -github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/httprc/v3 v3.0.0-beta2 h1:SDxjGoH7qj0nBXVrcrxX8eD94wEnjR+EEuqqmeqQYlY= +github.com/lestrrat-go/httprc/v3 v3.0.0-beta2/go.mod h1:Nwo81sMxE0DcvTB+rJyynNhv/DUu2yZErV7sscw9pHE= +github.com/lestrrat-go/jwx/v3 v3.0.1 h1:fH3T748FCMbXoF9UXXNS9i0q6PpYyJZK/rKSbkt2guY= +github.com/lestrrat-go/jwx/v3 v3.0.1/go.mod h1:XP2WqxMOSzHSyf3pfibCcfsLqbomxakAnNqiuaH8nwo= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -400,6 +393,8 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= @@ -412,12 +407,12 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -425,8 +420,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/teserakt-io/golang-ed25519 v0.0.0-20210104091850-3888c087a4c8 h1:RBkacARv7qY5laaXGlF4wFB/tk5rnthhPb8oIBGoagY= github.com/teserakt-io/golang-ed25519 v0.0.0-20210104091850-3888c087a4c8/go.mod h1:9PdLyPiZIiW3UopXyRnPYyjUXSpiQNHRLu8fOsR3o8M= github.com/tidwall/gjson v1.6.7/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI= @@ -500,9 +495,8 @@ golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -567,11 +561,10 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -588,8 +581,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -641,12 +634,12 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -654,8 +647,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -820,4 +813,4 @@ rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= -rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= \ No newline at end of file +rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= diff --git a/openapi/api_api.go b/openapi/api_api.go index 845dd2c2..e2c60552 100644 --- a/openapi/api_api.go +++ b/openapi/api_api.go @@ -38,6 +38,7 @@ var ErrorMessageNoCallback = ErrorMessage{"NoCallbackProvided", "A callback addr var ErrorMessageUnableToDecodeToken = ErrorMessage{"invalid_token", "Token could not be decoded."} var ErrorMessageUnableToDecodeCredential = ErrorMessage{"invalid_token", "Could not read the credential(s) inside the token."} var ErrorMessageUnableToDecodeHolder = ErrorMessage{"invalid_token", "Could not read the holder inside the token."} +var ErrorMessageNoSuchSession = ErrorMessage{"no_session", "Session with the requested id is not available."} func getApiVerifier() verifier.Verifier { if apiVerifier == nil { @@ -188,7 +189,13 @@ func StartSIOPSameDevice(c *gin.Context) { logging.Log().Infof("Start a login flow for a not specified client.") } - redirect, err := getApiVerifier().StartSameDeviceFlow(c.Request.Host, protocol, state, redirectPath, clientId) + requestMode, requestModeExists := c.GetQuery("request_mode") + if !requestModeExists { + logging.Log().Infof("Using default request mode %s.", DEFAULT_REQUEST_MODE) + requestMode = DEFAULT_REQUEST_MODE + } + + redirect, err := getApiVerifier().StartSameDeviceFlow(c.Request.Host, protocol, state, redirectPath, clientId, requestMode) if err != nil { logging.Log().Warnf("Error starting the same-device flow. Err: %v", err) c.AbortWithStatusJSON(500, ErrorMessage{err.Error(), "Was not able to start the same device flow."}) @@ -249,6 +256,20 @@ func GetVerifierAPIAuthenticationResponse(c *gin.Context) { handleAuthenticationResponse(c, state, presentation) } +// GetRequestByReference - Get the request object by reference +func GetRequestByReference(c *gin.Context) { + sessionId := c.Param("id") + + jwt, err := verifier.GetVerifier().GetRequestObject(sessionId) + if err != nil { + logging.Log().Debugf("No request for %s. Err: %v", sessionId, err) + c.AbortWithStatusJSON(404, ErrorMessageNoSuchSession) + return + } + + c.String(http.StatusOK, jwt) +} + func extractVpFromToken(c *gin.Context, vpToken string) (parsedPresentation *verifiable.Presentation, err error) { tokenBytes := decodeVpString(vpToken) @@ -327,7 +348,13 @@ func VerifierAPIStartSIOP(c *gin.Context) { logging.Log().Infof("Start a login flow for a not specified client.") } - connectionString, err := getApiVerifier().StartSiopFlow(c.Request.Host, protocol, callback, state, clientId) + requestMode, requestModeExists := c.GetQuery("request_mode") + if !requestModeExists { + logging.Log().Infof("Using default request mode %s.", DEFAULT_REQUEST_MODE) + requestMode = DEFAULT_REQUEST_MODE + } + + connectionString, err := getApiVerifier().StartSiopFlow(c.Request.Host, protocol, callback, state, clientId, requestMode) if err != nil { c.AbortWithStatusJSON(500, ErrorMessage{err.Error(), "Was not able to generate the connection string."}) return diff --git a/openapi/api_api_test.go b/openapi/api_api_test.go index c09fc808..414fd470 100644 --- a/openapi/api_api_test.go +++ b/openapi/api_api_test.go @@ -18,7 +18,7 @@ import ( "github.com/trustbloc/vc-go/verifiable" "github.com/gin-gonic/gin" - "github.com/lestrrat-go/jwx/jwk" + "github.com/lestrrat-go/jwx/v3/jwk" ) type mockVerifier struct { @@ -33,13 +33,13 @@ type mockVerifier struct { mockError error } -func (mV *mockVerifier) ReturnLoginQR(host string, protocol string, callback string, sessionId string, clientId string) (qr string, err error) { +func (mV *mockVerifier) ReturnLoginQR(host string, protocol string, callback string, sessionId string, clientId string, requestType string) (qr string, err error) { return mV.mockQR, mV.mockError } -func (mV *mockVerifier) StartSiopFlow(host string, protocol string, callback string, sessionId string, clientId string) (connectionString string, err error) { +func (mV *mockVerifier) StartSiopFlow(host string, protocol string, callback string, sessionId string, clientId string, requestType string) (connectionString string, err error) { return mV.mockConnectionString, mV.mockError } -func (mV *mockVerifier) StartSameDeviceFlow(host string, protocol string, sessionId string, redirectPath string, clientId string) (authenticationRequest string, err error) { +func (mV *mockVerifier) StartSameDeviceFlow(host string, protocol string, sessionId string, redirectPath string, clientId string, requestType string) (authenticationRequest string, err error) { return mV.mockAuthRequest, mV.mockError } func (mV *mockVerifier) GetToken(authorizationCode string, redirectUri string) (jwtString string, expiration int64, err error) { @@ -55,6 +55,11 @@ func (mV *mockVerifier) GetOpenIDConfiguration(serviceIdentifier string) (metada return mV.mockOpenIDConfig, err } +// TODO +func (mV *mockVerifier) GetRequestObject(state string) (jwt string, err error) { + return jwt, err +} + func (mV *mockVerifier) GenerateToken(clientId, subject, audience string, scope []string, presentation *verifiable.Presentation) (int64, string, error) { return mV.mockExpiration, mV.mockJWTString, mV.mockError } diff --git a/openapi/api_frontend.go b/openapi/api_frontend.go index 1737388c..e813588f 100644 --- a/openapi/api_frontend.go +++ b/openapi/api_frontend.go @@ -18,6 +18,8 @@ import ( "github.com/gin-gonic/gin" ) +const DEFAULT_REQUEST_MODE = verifier.REQUEST_MODE_URL_ENCODE + var frontendVerifier verifier.Verifier func getFrontendVerifier() verifier.Verifier { @@ -49,7 +51,13 @@ func VerifierPageDisplayQRSIOP(c *gin.Context) { logging.Log().Infof("Start a login flow for a not specified client.") } - qr, err := getFrontendVerifier().ReturnLoginQR(c.Request.Host, "https", callback, state, clientId) + requestMode, requestModeExists := c.GetQuery("request_mode") + if !requestModeExists { + logging.Log().Infof("Using default request mode %s.", DEFAULT_REQUEST_MODE) + requestMode = DEFAULT_REQUEST_MODE + } + + qr, err := getFrontendVerifier().ReturnLoginQR(c.Request.Host, "https", callback, state, clientId, requestMode) if err != nil { c.AbortWithStatusJSON(500, ErrorMessage{"qr_generation_error", err.Error()}) return diff --git a/openapi/routers.go b/openapi/routers.go index f5986b97..59b2b5a4 100644 --- a/openapi/routers.go +++ b/openapi/routers.go @@ -101,6 +101,13 @@ var routes = Routes{ VerifierAPIOpenIDConfiguration, }, + { + "GetRequestByReference", + http.MethodGet, + "/api/v1/request/:id", + GetRequestByReference, + }, + { "VerifierAPIStartSIOP", http.MethodGet, diff --git a/tir/tokenProvider.go b/tir/tokenProvider.go index 508ebbe0..378ecf97 100644 --- a/tir/tokenProvider.go +++ b/tir/tokenProvider.go @@ -7,7 +7,6 @@ import ( "encoding/base64" "errors" "net/http" - "os" common "github.com/fiware/VCVerifier/common" configModel "github.com/fiware/VCVerifier/config" @@ -27,7 +26,7 @@ import ( /** * Global file accessor */ -var localFileAccessor fileAccessor = diskFileAccessor{} +var localFileAccessor common.FileAccessor = common.DiskFileAccessor{} var ErrorTokenProviderNoKey = errors.New("no_key_configured") var ErrorTokenProviderNoVC = errors.New("no_vc_configured") @@ -193,17 +192,6 @@ func getCredential(vcPath string) (vc *verifiable.Credential, err error) { return vc, err } -// file system interfaces - -type fileAccessor interface { - ReadFile(filename string) ([]byte, error) -} -type diskFileAccessor struct{} - -func (diskFileAccessor) ReadFile(filename string) ([]byte, error) { - return os.ReadFile(filename) -} - // NewRS256Signer creates RS256Signer. func NewRS256Signer(privKey *rsa.PrivateKey) *RS256Signer { return &RS256Signer{ diff --git a/verifier/credentialsConfig.go b/verifier/credentialsConfig.go index 50a34096..249786f6 100644 --- a/verifier/credentialsConfig.go +++ b/verifier/credentialsConfig.go @@ -2,6 +2,7 @@ package verifier import ( "context" + "errors" "fmt" "net/url" "time" @@ -18,12 +19,16 @@ import ( const CACHE_EXPIRY = 60 +var ErrorNoPresentationDefinition = errors.New("no_presentation_definition_configured") + /** * Provides information about credentialTypes associated with services and there trust anchors. */ type CredentialsConfig interface { - // should return the list of credentialtypes to be requested via the scope parameter - GetScope(serviceIdentifier string) (credentialTypes []string, err error) + // should return the list of scopes to be requested via the scope parameter + GetScope(serviceIdentifier string) (scopes []string, err error) + // should return the presentationDefinition be requested via the scope parameter + GetPresentationDefinition(serviceIdentifier string, scope string) (presentationDefinition config.PresentationDefinition, err error) // get (EBSI TrustedIssuersRegistry compliant) endpoints for the given service/credential combination, to check its issued by a trusted participant. GetTrustedParticipantLists(serviceIdentifier string, scope string, credentialType string) (trustedIssuersRegistryUrl []config.TrustedParticipantsList, err error) // get (EBSI TrustedIssuersRegistry compliant) endpoints for the given service/credential combination, to check that credentials are issued by trusted issuers @@ -96,8 +101,8 @@ func (cc ServiceBackedCredentialsConfig) fillCache(context.Context) { var tirEndpoints []string - for serviceScope, credentials := range configuredService.ServiceScopes { - for _, credential := range credentials { + for serviceScope, scopeEntry := range configuredService.ServiceScopes { + for _, credential := range scopeEntry.Credentials { serviceIssuersLists, err := cc.GetTrustedIssuersLists(configuredService.Id, serviceScope, credential.Type) if err != nil { logging.Log().Errorf("failed caching issuers lists in fillCache(): %v", err) @@ -122,8 +127,7 @@ func (cc ServiceBackedCredentialsConfig) RequiredCredentialTypes(serviceIdentifi return []string{}, fmt.Errorf("no service %s configured", serviceIdentifier) } -// FIXME shall we return all scopes or just the default one? -func (cc ServiceBackedCredentialsConfig) GetScope(serviceIdentifier string) (credentialTypes []string, err error) { +func (cc ServiceBackedCredentialsConfig) GetScope(serviceIdentifier string) (scopes []string, err error) { cacheEntry, hit := common.GlobalCache.ServiceCache.Get(serviceIdentifier) if hit { logging.Log().Debugf("Found scope for %s", serviceIdentifier) @@ -134,6 +138,19 @@ func (cc ServiceBackedCredentialsConfig) GetScope(serviceIdentifier string) (cre return []string{}, nil } +func (cc ServiceBackedCredentialsConfig) GetPresentationDefinition(serviceIdentifier string, scope string) (presentationDefinition config.PresentationDefinition, err error) { + cacheEntry, hit := common.GlobalCache.ServiceCache.Get(serviceIdentifier) + if hit { + + logging.Log().Warnf("The definition %s", logging.PrettyPrintObject(presentationDefinition)) + + return cacheEntry.(config.ConfiguredService).GetPresentationDefinition(scope), nil + + } + logging.Log().Debugf("No presentation definition for %s - %s", serviceIdentifier, scope) + return presentationDefinition, ErrorNoPresentationDefinition +} + func (cc ServiceBackedCredentialsConfig) GetTrustedParticipantLists(serviceIdentifier string, scope string, credentialType string) (trustedIssuersRegistryUrl []config.TrustedParticipantsList, err error) { logging.Log().Debugf("Get participants list for %s - %s - %s.", serviceIdentifier, scope, credentialType) cacheEntry, hit := common.GlobalCache.ServiceCache.Get(serviceIdentifier) diff --git a/verifier/verifier.go b/verifier/verifier.go index f9844c82..5398bc2f 100644 --- a/verifier/verifier.go +++ b/verifier/verifier.go @@ -5,7 +5,9 @@ import ( "crypto/elliptic" "crypto/rand" "crypto/rsa" + "crypto/x509" "encoding/base64" + "encoding/pem" "errors" "fmt" "io" @@ -26,18 +28,27 @@ import ( client "github.com/fiware/dsba-pdp/http" - "github.com/lestrrat-go/jwx/jwa" - "github.com/lestrrat-go/jwx/jwk" - "github.com/lestrrat-go/jwx/jwt" + "github.com/lestrrat-go/jwx/v3/cert" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwk" + "github.com/lestrrat-go/jwx/v3/jws" + "github.com/lestrrat-go/jwx/v3/jwt" "github.com/patrickmn/go-cache" qrcode "github.com/skip2/go-qrcode" "github.com/valyala/fasttemplate" ) +const REQUEST_MODE_URL_ENCODE = "urlEncoded" +const REQUEST_MODE_BY_VALUE = "byValue" +const REQUEST_MODE_BY_REFERENCE = "byReference" +const REQUEST_OBJECT_TYP = "oauth-authz-req+jwt" + var ErrorNoDID = errors.New("no_did_configured") var ErrorNoTIR = errors.New("no_tir_configured") var ErrorUnsupportedKeyAlgorithm = errors.New("unsupported_key_algorithm") var ErrorUnsupportedValidationMode = errors.New("unsupported_validation_mode") +var ErrorSupportedModesNotSet = errors.New("no_supported_request_mode_set") +var ErrorNoSigningKey = errors.New("no_signing_key_available") var ErrorInvalidVC = errors.New("invalid_vc") var ErrorNoSuchSession = errors.New("no_such_session") var ErrorWrongGrantType = errors.New("wrong_grant_type") @@ -46,19 +57,24 @@ var ErrorRedirectUriMismatch = errors.New("redirect_uri_does_not_match") var ErrorVerficationContextSetup = errors.New("no_valid_verification_context") var ErrorTokenUnparsable = errors.New("unable_to_parse_token") var ErrorRequiredCredentialNotProvided = errors.New("required_credential_not_provided") +var ErrorUnsupportedRequestMode = errors.New("unsupported_request_mode") +var ErrorNoExpiration = errors.New("no_jwt_expiration_set") +var ErrorNoKeyId = errors.New("no_key_id_available") +var ErrorNoRequestObject = errors.New("no_request_object_available") // Actual implementation of the verfifier functionality // verifier interface type Verifier interface { - ReturnLoginQR(host string, protocol string, callback string, sessionId string, clientId string) (qr string, err error) - StartSiopFlow(host string, protocol string, callback string, sessionId string, clientId string) (connectionString string, err error) - StartSameDeviceFlow(host string, protocol string, sessionId string, redirectPath string, clientId string) (authenticationRequest string, err error) + ReturnLoginQR(host string, protocol string, callback string, sessionId string, clientId string, requestMode string) (qr string, err error) + StartSiopFlow(host string, protocol string, callback string, sessionId string, clientId string, requestMode string) (connectionString string, err error) + StartSameDeviceFlow(host string, protocol string, sessionId string, redirectPath string, clientId string, requestMode string) (authenticationRequest string, err error) GetToken(authorizationCode string, redirectUri string) (jwtString string, expiration int64, err error) GetJWKS() jwk.Set AuthenticationResponse(state string, verifiablePresentation *verifiable.Presentation) (sameDevice SameDeviceResponse, err error) GenerateToken(clientId, subject, audience string, scope []string, verifiablePresentation *verifiable.Presentation) (int64, string, error) GetOpenIDConfiguration(serviceIdentifier string) (metadata common.OpenIDProviderMetadata, err error) + GetRequestObject(state string) (jwt string, err error) } type ValidationService interface { @@ -92,6 +108,12 @@ type CredentialVerifier struct { validationServices []ValidationService // Algorithm to be used for signing the jwt signingAlgorithm string + // request modes supported by this instance of the verifier + supportedRequestModes []string + // Key for signing the request objects + requestSigningKey *jwk.Key + // Client identification for signing the request objects + clientIdentification configModel.ClientIdentification } // allow singleton access to the verifier @@ -100,6 +122,9 @@ var verifier Verifier // http client to be used var httpClient = client.HttpClient() +// file accessor to be used +var localFileAccessor common.FileAccessor = common.DiskFileAccessor{} + // interfaces and default implementations type ValidationContext interface{} @@ -177,6 +202,8 @@ type loginSession struct { sessionId string // clientId provided for the session clientId string + // requestObject created for the session + requestObject string } // struct to represent a token, accessible through the token endpoint @@ -257,6 +284,15 @@ func InitVerifier(config *configModel.Configuration) (err error) { logging.Log().Errorf("Was not able to initiate a signing key. Err: %v", err) return err } + + didSigningKey, err := getRequestSigningKey(verifierConfig.ClientIdentification.KeyPath, verifierConfig.ClientIdentification.Id) + if (slices.Contains(verifierConfig.SupportedModes, REQUEST_MODE_BY_VALUE) || slices.Contains(verifierConfig.SupportedModes, REQUEST_MODE_BY_REFERENCE)) && err != nil { + logging.Log().Errorf("Was not able to get a signing key, despite mode %s supported. Err: %v", REQUEST_MODE_BY_VALUE, err) + return err + } else { + err = nil + } + logging.Log().Warnf("Initiated key %s.", logging.PrettyPrintObject(key)) verifier = &CredentialVerifier{ (&config.Server).Host, @@ -276,6 +312,9 @@ func InitVerifier(config *configModel.Configuration) (err error) { &trustedIssuerVerificationService, }, verifierConfig.KeyAlgorithm, + verifierConfig.SupportedModes, + &didSigningKey, + verifierConfig.ClientIdentification, } logging.Log().Debug("Successfully initalized the verifier") @@ -285,10 +324,19 @@ func InitVerifier(config *configModel.Configuration) (err error) { /** * Initializes the cross-device login flow and returns all neccessary information as a qr-code **/ -func (v *CredentialVerifier) ReturnLoginQR(host string, protocol string, callback string, sessionId string, clientId string) (qr string, err error) { +func (v *CredentialVerifier) ReturnLoginQR(host string, protocol string, callback string, sessionId string, clientId string, requestMode string) (qr string, err error) { + + for _, v := range v.supportedRequestModes { + logging.Log().Warnf("Supported: %s", v) + } + + if !slices.Contains(v.supportedRequestModes, requestMode) { + logging.Log().Infof("QR with mode %s was requested, but only %v is supported.", requestMode, v.supportedRequestModes) + return qr, ErrorUnsupportedRequestMode + } logging.Log().Debugf("Generate a login qr for %s.", callback) - authenticationRequest, err := v.initSiopFlow(host, protocol, callback, sessionId, clientId) + authenticationRequest, err := v.initSiopFlow(host, protocol, callback, sessionId, clientId, requestMode) if err != nil { return qr, err @@ -304,20 +352,20 @@ func (v *CredentialVerifier) ReturnLoginQR(host string, protocol string, callbac /** * Starts a siop-flow and returns the required connection information **/ -func (v *CredentialVerifier) StartSiopFlow(host string, protocol string, callback string, sessionId string, clientId string) (connectionString string, err error) { - logging.Log().Debugf("Start a plain siop-flow fro %s.", callback) +func (v *CredentialVerifier) StartSiopFlow(host string, protocol string, callback string, sessionId string, clientId string, requestMode string) (connectionString string, err error) { + logging.Log().Debugf("Start a plain siop-flow for %s.", callback) - return v.initSiopFlow(host, protocol, callback, sessionId, clientId) + return v.initSiopFlow(host, protocol, callback, sessionId, clientId, requestMode) } /** * Starts a same-device siop-flow and returns the required redirection information **/ -func (v *CredentialVerifier) StartSameDeviceFlow(host string, protocol string, sessionId string, redirectPath string, clientId string) (authenticationRequest string, err error) { +func (v *CredentialVerifier) StartSameDeviceFlow(host string, protocol string, sessionId string, redirectPath string, clientId string, requestMode string) (authenticationRequest string, err error) { logging.Log().Debugf("Initiate samedevice flow for %s.", host) state := v.nonceGenerator.GenerateNonce() - loginSession := loginSession{true, fmt.Sprintf("%s://%s%s", protocol, host, redirectPath), sessionId, clientId} + loginSession := loginSession{true, fmt.Sprintf("%s://%s%s", protocol, host, redirectPath), sessionId, clientId, ""} err = v.sessionCache.Add(state, loginSession, cache.DefaultExpiration) if err != nil { logging.Log().Warnf("Was not able to store the login session %s in cache. Err: %v", logging.PrettyPrintObject(loginSession), err) @@ -326,8 +374,7 @@ func (v *CredentialVerifier) StartSameDeviceFlow(host string, protocol string, s redirectUri := fmt.Sprintf("%s://%s/api/v1/authentication_response", protocol, host) - walletUri := protocol + "://" + host + redirectPath - return v.createAuthenticationRequest(walletUri, redirectUri, state, clientId), err + return v.generateAuthenticationRequest(protocol+"://"+host+redirectPath, clientId, redirectUri, state, loginSession, requestMode) } /** @@ -353,17 +400,24 @@ func (v *CredentialVerifier) GetToken(authorizationCode string, redirectUri stri switch v.signingAlgorithm { case "RS256": - signatureAlgorithm = jwa.RS256 + signatureAlgorithm = jwa.RS256() case "ES256": - signatureAlgorithm = jwa.ES256 + signatureAlgorithm = jwa.ES256() } - jwtBytes, err := v.tokenSigner.Sign(tokenSession.token, signatureAlgorithm, v.signingKey) + jwtBytes, err := v.tokenSigner.Sign(tokenSession.token, jwt.WithKey(signatureAlgorithm, v.signingKey)) if err != nil { logging.Log().Warnf("Was not able to sign the token. Err: %v", err) return jwtString, expiration, err } - expiration = tokenSession.token.Expiration().Unix() - v.clock.Now().Unix() + + tokenExpiry, exists := tokenSession.token.Expiration() + if !exists { + logging.Log().Warn("Token does not have an expiration.") + return jwtString, expiration, ErrorNoExpiration + } + + expiration = tokenExpiry.Unix() - v.clock.Now().Unix() return string(jwtBytes), expiration, err } @@ -374,7 +428,7 @@ func (v *CredentialVerifier) GetToken(authorizationCode string, redirectUri stri func (v *CredentialVerifier) GetJWKS() jwk.Set { jwks := jwk.NewSet() publicKey, _ := v.signingKey.PublicKey() - jwks.Add(publicKey) + jwks.AddKey(publicKey) return jwks } @@ -444,17 +498,24 @@ func (v *CredentialVerifier) GenerateToken(clientId, subject, audience string, s logging.Log().Warnf("Was not able to create the token. Err: %v", err) return 0, "", err } - expiration := token.Expiration().Unix() - v.clock.Now().Unix() + + tokenExpiry, exists := token.Expiration() + if !exists { + logging.Log().Warn("Token does not have an expiration.") + return 0, "", ErrorNoExpiration + } + + expiration := tokenExpiry.Unix() - v.clock.Now().Unix() var signatureAlgorithm jwa.SignatureAlgorithm switch v.signingAlgorithm { case "RS256": - signatureAlgorithm = jwa.RS256 + signatureAlgorithm = jwa.RS256() case "ES256": - signatureAlgorithm = jwa.ES256 + signatureAlgorithm = jwa.ES256() } - tokenBytes, err := v.tokenSigner.Sign(token, signatureAlgorithm, v.signingKey) + tokenBytes, err := v.tokenSigner.Sign(token, jwt.WithKey(signatureAlgorithm, v.signingKey)) if err != nil { logging.Log().Warnf("Was not able to sign the token. Err: %v", err) return 0, "", err @@ -556,6 +617,25 @@ func (v *CredentialVerifier) AuthenticationResponse(state string, verifiablePres } } +func (v *CredentialVerifier) GetRequestObject(state string) (jwt string, err error) { + + logging.Log().Infof("Get session with id %s", state) + loginSessionInterface, hit := v.sessionCache.Get(state) + if !hit { + logging.Log().Debugf("No cache entry for session with id %s", state) + return jwt, ErrorNoRequestObject + } + + loginSession := loginSessionInterface.(loginSession) + + if loginSession.requestObject == "" { + logging.Log().Debugf("No request object for session with id %s", state) + return jwt, ErrorNoRequestObject + } + + return loginSession.requestObject, err +} + func (v *CredentialVerifier) getHolderValidationContext(clientId string, scope string, credentialTypes []string, holder string) (validationContext []HolderValidationContext, err error) { validationContexts := []HolderValidationContext{} for _, credentialType := range credentialTypes { @@ -668,10 +748,11 @@ func verifyChain(vcs []*verifiable.Credential) (bool, error) { } // initializes the cross-device siop flow -func (v *CredentialVerifier) initSiopFlow(host string, protocol string, callback string, sessionId string, clientId string) (authenticationRequest string, err error) { +func (v *CredentialVerifier) initSiopFlow(host string, protocol string, callback string, sessionId string, clientId string, requestMode string) (authenticationRequest string, err error) { + state := v.nonceGenerator.GenerateNonce() - loginSession := loginSession{false, callback, sessionId, clientId} + loginSession := loginSession{false, callback, sessionId, clientId, ""} err = v.sessionCache.Add(state, loginSession, cache.DefaultExpiration) if err != nil { @@ -679,16 +760,47 @@ func (v *CredentialVerifier) initSiopFlow(host string, protocol string, callback return authenticationRequest, err } redirectUri := fmt.Sprintf("%s://%s/api/v1/authentication_response", protocol, host) - authenticationRequest = v.createAuthenticationRequest("openid://", redirectUri, state, clientId) - logging.Log().Debugf("Authentication request is %s.", authenticationRequest) - return authenticationRequest, err + return v.generateAuthenticationRequest("openid4vp://", clientId, redirectUri, state, loginSession, requestMode) +} + +func (v *CredentialVerifier) generateAuthenticationRequest(base string, clientId string, redirectUri string, state string, loginSession loginSession, requestMode string) (authenticationRequest string, err error) { + if requestMode == REQUEST_MODE_URL_ENCODE { + authenticationRequest = v.createAuthenticationRequestUrlEncoded(base, redirectUri, state, clientId) + logging.Log().Debugf("Authentication request is %s.", authenticationRequest) + return authenticationRequest, err + } else if requestMode == REQUEST_MODE_BY_VALUE { + authenticationRequest, err = v.createAuthenticationRequestByValue(base, redirectUri, state, clientId) + if err != nil { + logging.Log().Warnf("Was not able to create the authentication request by value. Error: %v", err) + } else { + logging.Log().Debugf("Authentication request is %s.", authenticationRequest) + } + return authenticationRequest, err + } else if requestMode == REQUEST_MODE_BY_REFERENCE { + requestObject, err := v.createAuthenticationRequestObject(redirectUri, state, clientId) + loginSession.requestObject = string(requestObject[:]) + + logging.Log().Infof("Store session with id %s", state) + v.sessionCache.Set(state, loginSession, cache.DefaultExpiration) + + authenticationRequest = v.createAuthenticationRequestByReference(base, state) + if err != nil { + logging.Log().Warnf("Was not able to create the authentication request by reference. Error: %v", err) + } else { + logging.Log().Debugf("Authentication request is %s.", authenticationRequest) + } + + return authenticationRequest, err + } else { + return authenticationRequest, ErrorUnsupportedRequestMode + } } // generate a jwt, containing the credential and mandatory information as defined by the dsba-convergence func (v *CredentialVerifier) generateJWT(presentation *verifiable.Presentation, holder string, audience string) (generatedJwt jwt.Token, err error) { - jwtBuilder := jwt.NewBuilder().Issuer(v.did).Claim("client_id", v.did).Subject(holder).Audience([]string{audience}).Claim("kid", v.signingKey.KeyID()).Expiration(v.clock.Now().Add(time.Minute * 30)) + jwtBuilder := jwt.NewBuilder().Issuer(v.did).Claim("client_id", v.did).Subject(holder).Audience([]string{audience}).Expiration(v.clock.Now().Add(time.Minute * 30)) if len(presentation.Credentials()) > 1 { jwtBuilder.Claim("verifiablePresentation", presentation) @@ -706,7 +818,7 @@ func (v *CredentialVerifier) generateJWT(presentation *verifiable.Presentation, } // creates an authenticationRequest string from the given parameters -func (v *CredentialVerifier) createAuthenticationRequest(base string, redirect_uri string, state string, clientId string) string { +func (v *CredentialVerifier) createAuthenticationRequestUrlEncoded(base string, redirect_uri string, state string, clientId string) string { // We use a template to generate the final string template := "{{base}}?response_type=vp_token" + @@ -731,14 +843,121 @@ func (v *CredentialVerifier) createAuthenticationRequest(base string, redirect_u authRequest := t.ExecuteString(map[string]interface{}{ "base": base, "scope": scope, - "client_id": v.did, + "client_id": v.clientIdentification.Id, "redirect_uri": redirect_uri, "state": state, "nonce": v.nonceGenerator.GenerateNonce(), }) return authRequest +} +// creates an authenticationRequest string from the given parameters +func (v *CredentialVerifier) createAuthenticationRequestByReference(base string, state string) string { + + // We use a template to generate the final string + template := "{{base}}?client_id={{client_id}}" + + "&request_uri={{host}}/api/v1/request/{{state}}" + + "&request_uri_method=get" + + t := fasttemplate.New(template, "{{", "}}") + authRequest := t.ExecuteString(map[string]interface{}{ + "base": base, + "client_id": v.clientIdentification.Id, + "host": v.host, + "state": state, + }) + + return authRequest +} + +func (v *CredentialVerifier) createAuthenticationRequestObject(redirect_uri string, state string, clientId string) (requestObject []byte, err error) { + jwtBuilder := jwt.NewBuilder().Issuer(v.did) + jwtBuilder.Claim("response_type", "vp_token") + jwtBuilder.Claim("client_id", v.clientIdentification.Id) + jwtBuilder.Claim("redirect_uri", redirect_uri) + jwtBuilder.Claim("state", state) + jwtBuilder.Claim("scope", "openid") + jwtBuilder.Claim("nonce", v.nonceGenerator.GenerateNonce()) + jwtBuilder.Expiration(v.clock.Now().Add(time.Second * 30)) + + // TODO: decide if more than the default scope should be supported. + presentationDefinition, err := v.credentialsConfig.GetPresentationDefinition(clientId, "") + if err != nil { + logging.Log().Errorf("Was not able to get the presentationDefintion for %s. Err: %v", clientId, err) + return requestObject, err + } + logging.Log().Warnf("The definition %s", logging.PrettyPrintObject(presentationDefinition)) + jwtBuilder.Claim("presentation_definition", presentationDefinition) + + requestToken, err := jwtBuilder.Build() + if err != nil { + logging.Log().Errorf("Was not able to build a token. Err: %v", err) + return requestObject, err + } + + signingKeyAlgorithm := v.clientIdentification.KeyAlgorithm + keyAlgorithm, err := jwa.KeyAlgorithmFrom(signingKeyAlgorithm) + if err != nil { + logging.Log().Errorf("Request signing key does not have a valid algorithm. Error: %v", err) + return requestObject, ErrorNoSigningKey + } + + headers := jws.NewHeaders() + headers.Set("typ", REQUEST_OBJECT_TYP) + if v.clientIdentification.CertificatePath != "" { + certs, err := loadCertChainFromPEM(v.clientIdentification.CertificatePath) + if err != nil { + logging.Log().Errorf("Was not able to read the client certificate chain. Error: %v", err) + return requestObject, err + } + + x5cChain := cert.Chain{} + for _, cert := range certs { + x5cChain.AddString(base64.StdEncoding.EncodeToString(cert.Raw)) + } + + err = headers.Set("x5c", &x5cChain) + if err != nil { + logging.Log().Errorf("Was not able to set x5c. Error: %v", err) + return requestObject, err + } + } else { + logging.Log().Debug("No certificate chain for client identity") + } + var opts jwt.SignEncryptParseOption + + if signingKeyAlgorithm == "ES256" || signingKeyAlgorithm == "ES256K" || signingKeyAlgorithm == "ES384" || signingKeyAlgorithm == "ES512" { + convertedKey, _ := (*v.requestSigningKey).(jwk.ECDSAPrivateKey) + opts = jwt.WithKey(keyAlgorithm, convertedKey, jws.WithProtectedHeaders(headers)) + } else { + convertedKey, _ := (*v.requestSigningKey).(jwk.RSAPrivateKey) + opts = jwt.WithKey(keyAlgorithm, convertedKey, jws.WithProtectedHeaders(headers)) + } + + t, err := v.tokenSigner.Sign(requestToken, opts) + logging.Log().Debugf("The token object: %s", t) + return t, err + +} + +// creates an authenticationRequest string from the given parameters +func (v *CredentialVerifier) createAuthenticationRequestByValue(base string, redirect_uri string, state string, clientId string) (request string, err error) { + + // We use a template to generate the final string + template := "{{base}}?client_id={{client_id}}" + + "&request={{request}}" + + signedRequest, err := v.createAuthenticationRequestObject(redirect_uri, state, clientId) + + t := fasttemplate.New(template, "{{", "}}") + authRequest := t.ExecuteString(map[string]interface{}{ + "base": base, + "client_id": v.clientIdentification.Id, + "request": signedRequest, + }) + + return authRequest, err } // call back to the original initiator of the login-session, providing an authorization_code for token retrieval @@ -772,6 +991,25 @@ func getHostName(urlString string) (host string, err error) { return url.Host, err } +func getRequestSigningKey(keyPath string, clientId string) (key jwk.Key, err error) { + // read key file + rawKey, err := localFileAccessor.ReadFile(keyPath) + if err != nil { + logging.Log().Warnf("Was not able to read the key file from %s. err: %v", keyPath, err) + return key, err + } // parse key file + + key, err = jwk.ParseKey(rawKey, jwk.WithPEM(true)) + if err != nil { + logging.Log().Warnf("Was not able to parse the key %s. err: %v", rawKey, err) + return key, err + } + + key.Set("kid", clientId) + + return +} + // Initialize the private key of the verifier. Might need to be persisted in future iterations. func initPrivateKey(keyType string) (key jwk.Key, err error) { var newKey interface{} @@ -786,7 +1024,8 @@ func initPrivateKey(keyType string) (key jwk.Key, err error) { if err != nil { return nil, err } - key, err = jwk.New(newKey) + + key, err = jwk.Import(newKey) if err != nil { return nil, err } @@ -807,5 +1046,53 @@ func verifyConfig(verifierConfig *configModel.Verifier) error { if !slices.Contains(SupportedModes, verifierConfig.ValidationMode) { return ErrorUnsupportedValidationMode } + if len(verifierConfig.SupportedModes) == 0 { + return ErrorSupportedModesNotSet + } + return nil } + +func loadCertChainFromPEM(path string) ([]*x509.Certificate, error) { + data, err := localFileAccessor.ReadFile(path) + if err != nil { + return nil, err + } + + var certs []*x509.Certificate + for { + var block *pem.Block + block, data = pem.Decode(data) + if block == nil { + break + } + + if block.Type != "CERTIFICATE" { + continue + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse certificate: %w", err) + } + + certs = append(certs, cert) + } + + if len(certs) == 0 { + return nil, fmt.Errorf("no certificates found in PEM file") + } + + return certs, nil +} + +type RequestObject struct { + Issuer string `json:"iss"` + ResponseType string `json:"response_type"` + ClientId string `json:"client_id"` + RedirectUri string `json:"redirect_uri"` + Scope string `json:"scope"` + Nonce string `json:"nonce"` + State string `json:"state"` + //dcql_query to be supported in the future +} diff --git a/verifier/verifier_test.go b/verifier/verifier_test.go index 80b19db0..7754ae8f 100644 --- a/verifier/verifier_test.go +++ b/verifier/verifier_test.go @@ -1,15 +1,16 @@ package verifier import ( - "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/rsa" "errors" + "math/big" "net/http" "net/url" "slices" + "strings" "testing" "time" @@ -18,12 +19,12 @@ import ( "github.com/trustbloc/vc-go/verifiable" common "github.com/fiware/VCVerifier/common" + "github.com/fiware/VCVerifier/config" configModel "github.com/fiware/VCVerifier/config" logging "github.com/fiware/VCVerifier/logging" "github.com/google/go-cmp/cmp" - "github.com/lestrrat-go/jwx/jwa" - "github.com/lestrrat-go/jwx/jwk" - "github.com/lestrrat-go/jwx/jwt" + "github.com/lestrrat-go/jwx/v3/jwk" + "github.com/lestrrat-go/jwx/v3/jwt" "golang.org/x/exp/maps" ) @@ -38,7 +39,7 @@ func TestVerifyConfig(t *testing.T) { } tests := []test{ - {"If all mandatory parameters are present, verfication should succeed.", configModel.Verifier{Did: "did:key:verifier", TirAddress: "http:tir.de", ValidationMode: "none", KeyAlgorithm: "RS256"}, nil}, + {"If all mandatory parameters are present, verfication should succeed.", configModel.Verifier{Did: "did:key:verifier", TirAddress: "http:tir.de", ValidationMode: "none", KeyAlgorithm: "RS256", SupportedModes: []string{"urlEncoded"}}, nil}, {"If no TIR is configured, the verification should fail.", configModel.Verifier{Did: "did:key:verifier", ValidationMode: "none", KeyAlgorithm: "RS256"}, ErrorNoTIR}, {"If no DID is configured, the verification should fail.", configModel.Verifier{TirAddress: "http:tir.de", ValidationMode: "none", KeyAlgorithm: "RS256"}, ErrorNoDID}, {"If no DID and TIR is configured, the verification should fail.", configModel.Verifier{ValidationMode: "none", KeyAlgorithm: "RS256"}, ErrorNoDID}, @@ -59,6 +60,15 @@ func TestVerifyConfig(t *testing.T) { } +type mockFileAccessor struct { + mockFile []byte + mockError error +} + +func (mfa mockFileAccessor) ReadFile(filename string) ([]byte, error) { + return mfa.mockFile, mfa.mockError +} + type mockNonceGenerator struct { staticValues []string } @@ -85,47 +95,79 @@ type mockTokenCache struct { type mockCredentialConfig struct { // ServiceId->Scope->CredentialType-> TIR/TIL URLs - mockScopes map[string]map[string]map[string]configModel.Credential + mockScopes map[string]map[string]config.ScopeEntry mockError error } -func createMockCredentials(serviceId, scope, credentialType, url, holderClaim string, holderVerfication bool) map[string]map[string]map[string]configModel.Credential { - credential := configModel.Credential{TrustedParticipantsLists: []configModel.TrustedParticipantsList{{"ebsi", url}}, TrustedIssuersLists: []string{url}, HolderVerification: configModel.HolderVerification{Enabled: holderVerfication, Claim: holderClaim}} - return map[string]map[string]map[string]configModel.Credential{serviceId: {scope: map[string]configModel.Credential{credentialType: credential}}} +func createMockCredentials(serviceId, scope, credentialType, url, holderClaim string, holderVerfication bool) map[string]map[string]config.ScopeEntry { + credential := configModel.Credential{Type: credentialType, TrustedParticipantsLists: []configModel.TrustedParticipantsList{{"ebsi", url}}, TrustedIssuersLists: []string{url}, HolderVerification: configModel.HolderVerification{Enabled: holderVerfication, Claim: holderClaim}} + + entry := config.ScopeEntry{Credentials: []configModel.Credential{credential}} + + return map[string]map[string]config.ScopeEntry{serviceId: {scope: entry}} } -func (mcc mockCredentialConfig) GetScope(serviceIdentifier string) (credentialTypes []string, err error) { +func (mcc mockCredentialConfig) GetScope(serviceIdentifier string) (scopes []string, err error) { if mcc.mockError != nil { - return credentialTypes, mcc.mockError + return scopes, mcc.mockError } return maps.Keys(mcc.mockScopes[serviceIdentifier]), err } + +func (mcc mockCredentialConfig) GetPresentationDefinition(serviceIdentifier string, scope string) (presentationDefinition config.PresentationDefinition, err error) { + if mcc.mockError != nil { + return presentationDefinition, mcc.mockError + } + return mcc.mockScopes[serviceIdentifier][scope].PresentationDefinition, err +} + func (mcc mockCredentialConfig) GetTrustedParticipantLists(serviceIdentifier string, scope string, credentialType string) (trustedIssuersRegistryUrl []configModel.TrustedParticipantsList, err error) { if mcc.mockError != nil { return trustedIssuersRegistryUrl, mcc.mockError } - return mcc.mockScopes[serviceIdentifier][scope][credentialType].TrustedParticipantsLists, err + for _, credential := range mcc.mockScopes[serviceIdentifier][scope].Credentials { + if credential.Type == credentialType { + return credential.TrustedParticipantsLists, err + } + } + return trustedIssuersRegistryUrl, err } func (mcc mockCredentialConfig) GetTrustedIssuersLists(serviceIdentifier string, scope string, credentialType string) (trustedIssuersRegistryUrl []string, err error) { if mcc.mockError != nil { return trustedIssuersRegistryUrl, mcc.mockError } - return mcc.mockScopes[serviceIdentifier][scope][credentialType].TrustedIssuersLists, err + + for _, credential := range mcc.mockScopes[serviceIdentifier][scope].Credentials { + if credential.Type == credentialType { + return credential.TrustedIssuersLists, err + } + } + return trustedIssuersRegistryUrl, err } func (mcc mockCredentialConfig) RequiredCredentialTypes(serviceIdentifier string, scope string) (credentialTypes []string, err error) { if mcc.mockError != nil { return credentialTypes, mcc.mockError } - return maps.Keys(mcc.mockScopes[serviceIdentifier][scope]), err + var types = []string{} + for _, credential := range mcc.mockScopes[serviceIdentifier][scope].Credentials { + types = append(types, credential.Type) + } + return types, err } func (mcc mockCredentialConfig) GetHolderVerification(serviceIdentifier string, scope string, credentialType string) (isEnabled bool, holderClaim string, err error) { if mcc.mockError != nil { return isEnabled, holderClaim, mcc.mockError } - credential := mcc.mockScopes[serviceIdentifier][scope][credentialType] - return credential.HolderVerification.Enabled, credential.HolderVerification.Claim, err + + for _, credential := range mcc.mockScopes[serviceIdentifier][scope].Credentials { + if credential.Type == credentialType { + + return credential.HolderVerification.Enabled, credential.HolderVerification.Claim, err + } + } + return isEnabled, holderClaim, err } func (msc *mockSessionCache) Add(k string, x interface{}, d time.Duration) error { @@ -171,18 +213,20 @@ func (mtc *mockTokenCache) Delete(k string) { } type siopInitTest struct { - testName string - testHost string - testProtocol string - testAddress string - testSessionId string - testClientId string - credentialScopes map[string]map[string]map[string]configModel.Credential - mockConfigError error - expectedCallback string - expectedConnection string - sessionCacheError error - expectedError error + testName string + testHost string + testProtocol string + testAddress string + testSessionId string + testClientId string + testRequestObjectJwt string + requestMode string + credentialScopes map[string]map[string]config.ScopeEntry + mockConfigError error + expectedCallback string + expectedConnection string + sessionCacheError error + expectedError error } func TestInitSiopFlow(t *testing.T) { @@ -196,8 +240,8 @@ func TestInitSiopFlow(t *testing.T) { sessionCache := mockSessionCache{sessions: map[string]loginSession{}, errorToThrow: tc.sessionCacheError} nonceGenerator := mockNonceGenerator{staticValues: []string{"randomState", "randomNonce"}} credentialsConfig := mockCredentialConfig{tc.credentialScopes, tc.mockConfigError} - verifier := CredentialVerifier{did: "did:key:verifier", sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, credentialsConfig: credentialsConfig} - authReq, err := verifier.initSiopFlow(tc.testHost, tc.testProtocol, tc.testAddress, tc.testSessionId, tc.testClientId) + verifier := CredentialVerifier{did: "did:key:verifier", sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, credentialsConfig: credentialsConfig, clientIdentification: configModel.ClientIdentification{Id: "did:key:verifier"}} + authReq, err := verifier.initSiopFlow(tc.testHost, tc.testProtocol, tc.testAddress, tc.testSessionId, tc.testClientId, "urlEncoded") verifyInitTest(t, tc, authReq, err, sessionCache, false) }) } @@ -208,6 +252,8 @@ func TestStartSiopFlow(t *testing.T) { logging.Configure(true, "DEBUG", true, []string{}) + testKey := getECDSAKey() + tests := getInitSiopTests() for _, tc := range tests { t.Run(tc.testName, func(t *testing.T) { @@ -215,8 +261,8 @@ func TestStartSiopFlow(t *testing.T) { sessionCache := mockSessionCache{sessions: map[string]loginSession{}, errorToThrow: tc.sessionCacheError} nonceGenerator := mockNonceGenerator{staticValues: []string{"randomState", "randomNonce"}} credentialsConfig := mockCredentialConfig{tc.credentialScopes, tc.mockConfigError} - verifier := CredentialVerifier{did: "did:key:verifier", sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, credentialsConfig: credentialsConfig} - authReq, err := verifier.StartSiopFlow(tc.testHost, tc.testProtocol, tc.testAddress, tc.testSessionId, tc.testClientId) + verifier := CredentialVerifier{did: "did:key:verifier", sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, tokenSigner: mockTokenSigner{}, clock: mockClock{}, requestSigningKey: &testKey, credentialsConfig: credentialsConfig, clientIdentification: configModel.ClientIdentification{Id: "did:key:verifier", KeyPath: "/my-signing-key.pem", KeyAlgorithm: "ES256"}} + authReq, err := verifier.StartSiopFlow(tc.testHost, tc.testProtocol, tc.testAddress, tc.testSessionId, tc.testClientId, tc.requestMode) verifyInitTest(t, tc, authReq, err, sessionCache, false) }) } @@ -230,35 +276,63 @@ func verifyInitTest(t *testing.T, tc siopInitTest, authRequest string, err error // if the error was successfully verfied, we can just continue return } - if authRequest != tc.expectedConnection { - t.Errorf("%s - Expected %s but was %s.", tc.testName, tc.expectedConnection, authRequest) + // in this case the request contains a JWT. Due to the indeterminism of ECDSA signatures a plain compare wont do it here. + if tc.requestMode == REQUEST_MODE_BY_VALUE { + + // we know that the last part should be the jwt, thus just removing the signature part(e.g. everything after the last dot) is enough + cleanedRequest := removeSignature(authRequest) + if cleanedRequest != tc.expectedConnection { + t.Errorf("%s - Expected %s but was %s", tc.testName, tc.expectedConnection, cleanedRequest) + } + } + + if authRequest != tc.expectedConnection && tc.requestMode != REQUEST_MODE_BY_VALUE { + t.Errorf("%s - Expected %s but was %s", tc.testName, tc.expectedConnection, authRequest) } cachedSession, found := sessionCache.sessions["randomState"] if !found { t.Errorf("%s - A login session should have been stored.", tc.testName) } - expectedSession := loginSession{sameDevice, tc.expectedCallback, tc.testSessionId, tc.testClientId} + var expectedSession loginSession + if tc.requestMode == REQUEST_MODE_BY_REFERENCE { + expectedSession = loginSession{sameDevice, tc.expectedCallback, tc.testSessionId, tc.testClientId, tc.testRequestObjectJwt} + cachedSession.requestObject = removeSignature(cachedSession.requestObject) + } else { + expectedSession = loginSession{sameDevice, tc.expectedCallback, tc.testSessionId, tc.testClientId, tc.testRequestObjectJwt} + } if cachedSession != expectedSession { t.Errorf("%s - The login session was expected to be %v but was %v.", tc.testName, expectedSession, cachedSession) } } +func removeSignature(jwt string) string { + splitted := strings.Split(jwt, ".") + splitted = splitted[:len(splitted)-1] + return strings.Join(splitted, ".") +} + func getInitSiopTests() []siopInitTest { cacheFailError := errors.New("cache_fail") return []siopInitTest{ - {testName: "If all parameters are set, a proper connection string should be returned.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "", credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://client.org/callback", - expectedConnection: "openid://?response_type=vp_token&response_mode=direct_post&client_id=did:key:verifier&redirect_uri=https://verifier.org/api/v1/authentication_response&state=randomState&nonce=randomNonce", sessionCacheError: nil, expectedError: nil, + {testName: "If all parameters are set, a proper connection string should be returned.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "", requestMode: REQUEST_MODE_URL_ENCODE, credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://client.org/callback", + expectedConnection: "openid4vp://?response_type=vp_token&response_mode=direct_post&client_id=did:key:verifier&redirect_uri=https://verifier.org/api/v1/authentication_response&state=randomState&nonce=randomNonce", sessionCacheError: nil, expectedError: nil, }, - {testName: "The scope should be included if configured.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "myService", credentialScopes: createMockCredentials("myService", "someScope", "org.fiware.MySpecialCredential", "some.url", "", false), mockConfigError: nil, expectedCallback: "https://client.org/callback", - expectedConnection: "openid://?response_type=vp_token&response_mode=direct_post&client_id=did:key:verifier&redirect_uri=https://verifier.org/api/v1/authentication_response&state=randomState&nonce=randomNonce&scope=someScope", sessionCacheError: nil, expectedError: nil, + {testName: "The scope should be included if configured.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "myService", requestMode: REQUEST_MODE_URL_ENCODE, credentialScopes: createMockCredentials("myService", "someScope", "org.fiware.MySpecialCredential", "some.url", "", false), mockConfigError: nil, expectedCallback: "https://client.org/callback", + expectedConnection: "openid4vp://?response_type=vp_token&response_mode=direct_post&client_id=did:key:verifier&redirect_uri=https://verifier.org/api/v1/authentication_response&state=randomState&nonce=randomNonce&scope=someScope", sessionCacheError: nil, expectedError: nil, }, - {testName: "If the login-session could not be cached, an error should be thrown.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "", credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://client.org/callback", + {testName: "If the login-session could not be cached, an error should be thrown.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "", requestMode: REQUEST_MODE_URL_ENCODE, credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://client.org/callback", expectedConnection: "", sessionCacheError: cacheFailError, expectedError: cacheFailError, }, - {testName: "If config service throws an error, no scope should be included.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "myService", credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: errors.New("config_error"), expectedCallback: "https://client.org/callback", - expectedConnection: "openid://?response_type=vp_token&response_mode=direct_post&client_id=did:key:verifier&redirect_uri=https://verifier.org/api/v1/authentication_response&state=randomState&nonce=randomNonce", sessionCacheError: nil, expectedError: nil, + {testName: "If config service throws an error, no scope should be included.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "myService", requestMode: REQUEST_MODE_URL_ENCODE, credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: errors.New("config_error"), expectedCallback: "https://client.org/callback", + expectedConnection: "openid4vp://?response_type=vp_token&response_mode=direct_post&client_id=did:key:verifier&redirect_uri=https://verifier.org/api/v1/authentication_response&state=randomState&nonce=randomNonce", sessionCacheError: nil, expectedError: nil, + }, + {testName: "If all parameters are set, a proper connection string byValue should be returned.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "", requestMode: REQUEST_MODE_BY_VALUE, credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://client.org/callback", + expectedConnection: "openid4vp://?client_id=did:key:verifier&request=eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnZlcmlmaWVyIiwiZXhwIjozMCwiaXNzIjoiZGlkOmtleTp2ZXJpZmllciIsIm5vbmNlIjoicmFuZG9tTm9uY2UiLCJwcmVzZW50YXRpb25fZGVmaW5pdGlvbiI6eyJpZCI6IiIsImlucHV0X2Rlc2NyaXB0b3JzIjpudWxsLCJmb3JtYXQiOm51bGx9LCJyZWRpcmVjdF91cmkiOiJodHRwczovL3ZlcmlmaWVyLm9yZy9hcGkvdjEvYXV0aGVudGljYXRpb25fcmVzcG9uc2UiLCJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJzY29wZSI6Im9wZW5pZCIsInN0YXRlIjoicmFuZG9tU3RhdGUifQ", sessionCacheError: nil, expectedError: nil, + }, + {testName: "If all parameters are set, a proper connection string byReference should be returned.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "", requestMode: REQUEST_MODE_BY_REFERENCE, credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://client.org/callback", + expectedConnection: "openid4vp://?client_id=did:key:verifier&request_uri=/api/v1/request/randomState&request_uri_method=get", sessionCacheError: nil, expectedError: nil, testRequestObjectJwt: "eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnZlcmlmaWVyIiwiZXhwIjozMCwiaXNzIjoiZGlkOmtleTp2ZXJpZmllciIsIm5vbmNlIjoicmFuZG9tTm9uY2UiLCJwcmVzZW50YXRpb25fZGVmaW5pdGlvbiI6eyJpZCI6IiIsImlucHV0X2Rlc2NyaXB0b3JzIjpudWxsLCJmb3JtYXQiOm51bGx9LCJyZWRpcmVjdF91cmkiOiJodHRwczovL3ZlcmlmaWVyLm9yZy9hcGkvdjEvYXV0aGVudGljYXRpb25fcmVzcG9uc2UiLCJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJzY29wZSI6Im9wZW5pZCIsInN0YXRlIjoicmFuZG9tU3RhdGUifQ", }, } } @@ -268,15 +342,23 @@ func TestStartSameDeviceFlow(t *testing.T) { cacheFailError := errors.New("cache_fail") logging.Configure(true, "DEBUG", true, []string{}) + testKey := getECDSAKey() + tests := []siopInitTest{ - {testName: "If everything is provided, a samedevice flow should be started.", testHost: "myhost.org", testProtocol: "https", testAddress: "/redirect", testSessionId: "my-random-session-id", testClientId: "", credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://myhost.org/redirect", - expectedConnection: "https://myhost.org/redirect?response_type=vp_token&response_mode=direct_post&client_id=did:key:verifier&redirect_uri=https://myhost.org/api/v1/authentication_response&state=randomState&nonce=randomNonce", sessionCacheError: nil, expectedError: nil, + {testName: "If everything is provided, a samedevice flow should be started.", testHost: "verifier.org", testProtocol: "https", testAddress: "/redirect", testSessionId: "my-random-session-id", testClientId: "", credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://verifier.org/redirect", + requestMode: REQUEST_MODE_URL_ENCODE, expectedConnection: "https://verifier.org/redirect?response_type=vp_token&response_mode=direct_post&client_id=did:key:verifier&redirect_uri=https://verifier.org/api/v1/authentication_response&state=randomState&nonce=randomNonce", sessionCacheError: nil, expectedError: nil, }, - {testName: "The scope should be included if configured.", testHost: "myhost.org", testProtocol: "https", testAddress: "/redirect", testSessionId: "my-random-session-id", testClientId: "myService", credentialScopes: createMockCredentials("myService", "someScope", "org.fiware.MySpecialCredential", "some.url", "", false), mockConfigError: nil, expectedCallback: "https://myhost.org/redirect", - expectedConnection: "https://myhost.org/redirect?response_type=vp_token&response_mode=direct_post&client_id=did:key:verifier&redirect_uri=https://myhost.org/api/v1/authentication_response&state=randomState&nonce=randomNonce&scope=someScope", sessionCacheError: nil, expectedError: nil, + {testName: "The scope should be included if configured.", testHost: "verifier.org", testProtocol: "https", testAddress: "/redirect", testSessionId: "my-random-session-id", testClientId: "myService", credentialScopes: createMockCredentials("myService", "someScope", "org.fiware.MySpecialCredential", "some.url", "", false), mockConfigError: nil, expectedCallback: "https://verifier.org/redirect", + requestMode: REQUEST_MODE_URL_ENCODE, expectedConnection: "https://verifier.org/redirect?response_type=vp_token&response_mode=direct_post&client_id=did:key:verifier&redirect_uri=https://verifier.org/api/v1/authentication_response&state=randomState&nonce=randomNonce&scope=someScope", sessionCacheError: nil, expectedError: nil, }, - {testName: "If the request cannot be cached, an error should be responded.", testHost: "myhost.org", testProtocol: "https", testAddress: "/redirect", testSessionId: "my-random-session-id", testClientId: "", credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://myhost.org/redirect", - expectedConnection: "", sessionCacheError: cacheFailError, expectedError: cacheFailError, + {testName: "If the request cannot be cached, an error should be responded.", testHost: "verifier.org", testProtocol: "https", testAddress: "/redirect", testSessionId: "my-random-session-id", testClientId: "", credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://verifier.org/redirect", + requestMode: REQUEST_MODE_URL_ENCODE, expectedConnection: "", sessionCacheError: cacheFailError, expectedError: cacheFailError, + }, + {testName: "If everything is provided, a samedevice flow should be started.", testHost: "verifier.org", testProtocol: "https", testAddress: "/redirect", testSessionId: "my-random-session-id", testClientId: "", credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://verifier.org/redirect", + requestMode: REQUEST_MODE_BY_VALUE, expectedConnection: "https://verifier.org/redirect?client_id=did:key:verifier&request=eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnZlcmlmaWVyIiwiZXhwIjozMCwiaXNzIjoiZGlkOmtleTp2ZXJpZmllciIsIm5vbmNlIjoicmFuZG9tTm9uY2UiLCJwcmVzZW50YXRpb25fZGVmaW5pdGlvbiI6eyJpZCI6IiIsImlucHV0X2Rlc2NyaXB0b3JzIjpudWxsLCJmb3JtYXQiOm51bGx9LCJyZWRpcmVjdF91cmkiOiJodHRwczovL3ZlcmlmaWVyLm9yZy9hcGkvdjEvYXV0aGVudGljYXRpb25fcmVzcG9uc2UiLCJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJzY29wZSI6Im9wZW5pZCIsInN0YXRlIjoicmFuZG9tU3RhdGUifQ", sessionCacheError: nil, expectedError: nil, + }, + {testName: "If everything is provided, a samedevice flow should be started.", testHost: "verifier.org", testProtocol: "https", testAddress: "/redirect", testSessionId: "my-random-session-id", testClientId: "", credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://verifier.org/redirect", + requestMode: REQUEST_MODE_BY_REFERENCE, expectedConnection: "https://verifier.org/redirect?client_id=did:key:verifier&request_uri=/api/v1/request/randomState&request_uri_method=get", sessionCacheError: nil, expectedError: nil, testRequestObjectJwt: "eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnZlcmlmaWVyIiwiZXhwIjozMCwiaXNzIjoiZGlkOmtleTp2ZXJpZmllciIsIm5vbmNlIjoicmFuZG9tTm9uY2UiLCJwcmVzZW50YXRpb25fZGVmaW5pdGlvbiI6eyJpZCI6IiIsImlucHV0X2Rlc2NyaXB0b3JzIjpudWxsLCJmb3JtYXQiOm51bGx9LCJyZWRpcmVjdF91cmkiOiJodHRwczovL3ZlcmlmaWVyLm9yZy9hcGkvdjEvYXV0aGVudGljYXRpb25fcmVzcG9uc2UiLCJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJzY29wZSI6Im9wZW5pZCIsInN0YXRlIjoicmFuZG9tU3RhdGUifQ", }, } @@ -286,8 +368,8 @@ func TestStartSameDeviceFlow(t *testing.T) { sessionCache := mockSessionCache{sessions: map[string]loginSession{}, errorToThrow: tc.sessionCacheError} nonceGenerator := mockNonceGenerator{staticValues: []string{"randomState", "randomNonce"}} credentialsConfig := mockCredentialConfig{tc.credentialScopes, tc.mockConfigError} - verifier := CredentialVerifier{did: "did:key:verifier", sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, credentialsConfig: credentialsConfig} - authReq, err := verifier.StartSameDeviceFlow(tc.testHost, tc.testProtocol, tc.testSessionId, tc.testAddress, tc.testClientId) + verifier := CredentialVerifier{did: "did:key:verifier", sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, tokenSigner: mockTokenSigner{}, clock: mockClock{}, requestSigningKey: &testKey, credentialsConfig: credentialsConfig, clientIdentification: configModel.ClientIdentification{Id: "did:key:verifier", KeyPath: "/my-signing-key.pem", KeyAlgorithm: "ES256"}} + authReq, err := verifier.StartSameDeviceFlow(tc.testHost, tc.testProtocol, tc.testSessionId, tc.testAddress, tc.testClientId, tc.requestMode) verifyInitTest(t, tc, authReq, err, sessionCache, true) }) } @@ -357,26 +439,26 @@ func TestAuthenticationResponse(t *testing.T) { tests := []authTest{ // general behaviour - {"If the credential is invalid, return an error.", true, "login-state", getVP([]string{"vc"}), "holder", loginSession{true, "https://myhost.org/callback", "my-session", "clientId"}, "login-state", nil, []bool{false}, nil, SameDeviceResponse{}, nil, ErrorInvalidVC, nil}, - {"If one credential is invalid, return an error.", true, "login-state", getVP([]string{"vc1", "vc2"}), "holder", loginSession{true, "https://myhost.org/callback", "my-session", "clientId"}, "login-state", nil, []bool{true, false}, nil, SameDeviceResponse{}, nil, ErrorInvalidVC, nil}, + {"If the credential is invalid, return an error.", true, "login-state", getVP([]string{"vc"}), "holder", loginSession{true, "https://myhost.org/callback", "my-session", "clientId", "requestObjectJwt"}, "login-state", nil, []bool{false}, nil, SameDeviceResponse{}, nil, ErrorInvalidVC, nil}, + {"If one credential is invalid, return an error.", true, "login-state", getVP([]string{"vc1", "vc2"}), "holder", loginSession{true, "https://myhost.org/callback", "my-session", "clientId", "requestObjectJwt"}, "login-state", nil, []bool{true, false}, nil, SameDeviceResponse{}, nil, ErrorInvalidVC, nil}, {"If an authentication response is received without a session, an error should be responded.", true, "", getVP([]string{"vc"}), "holder", loginSession{}, "login-state", nil, []bool{}, nil, SameDeviceResponse{}, nil, ErrorNoSuchSession, nil}, - {"If ssiKit throws an error, an error should be responded.", true, "login-state", getVP([]string{"vc"}), "holder", loginSession{true, "https://myhost.org/callback", "my-session", "clientId"}, "login-state", nil, []bool{}, ssiKitError, SameDeviceResponse{}, nil, ssiKitError, nil}, - {"If tokenCache throws an error, an error should be responded.", true, "login-state", getVP([]string{"vc"}), "holder", loginSession{true, "https://myhost.org/callback", "my-session", "clientId"}, "login-state", nil, []bool{true}, nil, SameDeviceResponse{}, nil, cacheError, cacheError}, - {"If the credential is invalid, return an error.", false, "login-state", getVP([]string{"vc"}), "holder", loginSession{false, "https://myhost.org/callback", "my-session", "clientId"}, "login-state", nil, []bool{false}, nil, SameDeviceResponse{}, nil, ErrorInvalidVC, nil}, - {"If one credential is invalid, return an error.", false, "login-state", getVP([]string{"vc1", "vc2"}), "holder", loginSession{false, "https://myhost.org/callback", "my-session", "clientId"}, "login-state", nil, []bool{true, false}, nil, SameDeviceResponse{}, nil, ErrorInvalidVC, nil}, + {"If ssiKit throws an error, an error should be responded.", true, "login-state", getVP([]string{"vc"}), "holder", loginSession{true, "https://myhost.org/callback", "my-session", "clientId", "requestObjectJwt"}, "login-state", nil, []bool{}, ssiKitError, SameDeviceResponse{}, nil, ssiKitError, nil}, + {"If tokenCache throws an error, an error should be responded.", true, "login-state", getVP([]string{"vc"}), "holder", loginSession{true, "https://myhost.org/callback", "my-session", "clientId", "requestObjectJwt"}, "login-state", nil, []bool{true}, nil, SameDeviceResponse{}, nil, cacheError, cacheError}, + {"If the credential is invalid, return an error.", false, "login-state", getVP([]string{"vc"}), "holder", loginSession{false, "https://myhost.org/callback", "my-session", "clientId", "requestObjectJwt"}, "login-state", nil, []bool{false}, nil, SameDeviceResponse{}, nil, ErrorInvalidVC, nil}, + {"If one credential is invalid, return an error.", false, "login-state", getVP([]string{"vc1", "vc2"}), "holder", loginSession{false, "https://myhost.org/callback", "my-session", "clientId", "requestObjectJwt"}, "login-state", nil, []bool{true, false}, nil, SameDeviceResponse{}, nil, ErrorInvalidVC, nil}, {"If an authentication response is received without a session, an error should be responded.", false, "", getVP([]string{"vc"}), "holder", loginSession{}, "login-state", nil, []bool{}, nil, SameDeviceResponse{}, nil, ErrorNoSuchSession, nil}, - {"If ssiKit throws an error, an error should be responded.", false, "login-state", getVP([]string{"vc"}), "holder", loginSession{false, "https://myhost.org/callback", "my-session", "clientId"}, "login-state", nil, []bool{}, ssiKitError, SameDeviceResponse{}, nil, ssiKitError, nil}, - {"If tokenCache throws an error, an error should be responded.", false, "login-state", getVP([]string{"vc"}), "holder", loginSession{false, "https://myhost.org/callback", "my-session", "clientId"}, "login-state", nil, []bool{true}, nil, SameDeviceResponse{}, nil, cacheError, cacheError}, - {"If a non-existent session is requested, an error should be responded.", false, "login-state", getVP([]string{"vc"}), "holder", loginSession{false, "https://myhost.org/callback", "my-session", "clientId"}, "non-existent-state", nil, []bool{true}, nil, SameDeviceResponse{}, nil, ErrorNoSuchSession, nil}, + {"If ssiKit throws an error, an error should be responded.", false, "login-state", getVP([]string{"vc"}), "holder", loginSession{false, "https://myhost.org/callback", "my-session", "clientId", "requestObjectJwt"}, "login-state", nil, []bool{}, ssiKitError, SameDeviceResponse{}, nil, ssiKitError, nil}, + {"If tokenCache throws an error, an error should be responded.", false, "login-state", getVP([]string{"vc"}), "holder", loginSession{false, "https://myhost.org/callback", "my-session", "clientId", "requestObjectJwt"}, "login-state", nil, []bool{true}, nil, SameDeviceResponse{}, nil, cacheError, cacheError}, + {"If a non-existent session is requested, an error should be responded.", false, "login-state", getVP([]string{"vc"}), "holder", loginSession{false, "https://myhost.org/callback", "my-session", "clientId", "requestObjectJwt"}, "non-existent-state", nil, []bool{true}, nil, SameDeviceResponse{}, nil, ErrorNoSuchSession, nil}, // same-device flow - {"When a same device flow is present, a proper response should be returned.", true, "login-state", getVP([]string{"vc"}), "holder", loginSession{true, "https://myhost.org/callback", "my-session", "clientId"}, "login-state", nil, []bool{true}, nil, SameDeviceResponse{"https://myhost.org/callback", "authCode", "my-session"}, nil, nil, nil}, - {"When a same device flow is present, a proper response should be returned for VPs.", true, "login-state", getVP([]string{"vc1", "vc2"}), "holder", loginSession{true, "https://myhost.org/callback", "my-session", "clientId"}, "login-state", nil, []bool{true, true}, nil, SameDeviceResponse{"https://myhost.org/callback", "authCode", "my-session"}, nil, nil, nil}, + {"When a same device flow is present, a proper response should be returned.", true, "login-state", getVP([]string{"vc"}), "holder", loginSession{true, "https://myhost.org/callback", "my-session", "clientId", "requestObjectJwt"}, "login-state", nil, []bool{true}, nil, SameDeviceResponse{"https://myhost.org/callback", "authCode", "my-session"}, nil, nil, nil}, + {"When a same device flow is present, a proper response should be returned for VPs.", true, "login-state", getVP([]string{"vc1", "vc2"}), "holder", loginSession{true, "https://myhost.org/callback", "my-session", "clientId", "requestObjectJwt"}, "login-state", nil, []bool{true, true}, nil, SameDeviceResponse{"https://myhost.org/callback", "authCode", "my-session"}, nil, nil, nil}, // cross-device flow - {"When a cross-device flow is present, a proper response should be sent to the requestors callback.", false, "login-state", getVP([]string{"vc"}), "holder", loginSession{false, "https://myhost.org/callback", "my-session", "clientId"}, "login-state", nil, []bool{true}, nil, SameDeviceResponse{}, getRequest("https://myhost.org/callback?code=authCode&state=my-session"), nil, nil}, - {"When a cross-device flow is present, a proper response should be sent to the requestors callback for VPs.", false, "login-state", getVP([]string{"vc1", "vc2"}), "holder", loginSession{false, "https://myhost.org/callback", "my-session", "clientId"}, "login-state", nil, []bool{true, true}, nil, SameDeviceResponse{}, getRequest("https://myhost.org/callback?code=authCode&state=my-session"), nil, nil}, - {"When the requestor-callback fails, an error should be returned.", false, "login-state", getVP([]string{"vc"}), "holder", loginSession{false, "https://myhost.org/callback", "my-session", "clientId"}, "login-state", callbackError, []bool{true}, nil, SameDeviceResponse{}, nil, callbackError, nil}, + {"When a cross-device flow is present, a proper response should be sent to the requestors callback.", false, "login-state", getVP([]string{"vc"}), "holder", loginSession{false, "https://myhost.org/callback", "my-session", "clientId", "requestObjectJwt"}, "login-state", nil, []bool{true}, nil, SameDeviceResponse{}, getRequest("https://myhost.org/callback?code=authCode&state=my-session"), nil, nil}, + {"When a cross-device flow is present, a proper response should be sent to the requestors callback for VPs.", false, "login-state", getVP([]string{"vc1", "vc2"}), "holder", loginSession{false, "https://myhost.org/callback", "my-session", "clientId", "requestObjectJwt"}, "login-state", nil, []bool{true, true}, nil, SameDeviceResponse{}, getRequest("https://myhost.org/callback?code=authCode&state=my-session"), nil, nil}, + {"When the requestor-callback fails, an error should be returned.", false, "login-state", getVP([]string{"vc"}), "holder", loginSession{false, "https://myhost.org/callback", "my-session", "clientId", "requestObjectJwt"}, "login-state", callbackError, []bool{true}, nil, SameDeviceResponse{}, nil, callbackError, nil}, } for _, tc := range tests { @@ -393,11 +475,11 @@ func TestAuthenticationResponse(t *testing.T) { httpClient = mockHttpClient{tc.callbackError, nil} ecdsKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - testKey, _ := jwk.New(ecdsKey) + testKey, _ := jwk.Import(ecdsKey) jwk.AssignKeyID(testKey) nonceGenerator := mockNonceGenerator{staticValues: []string{"authCode"}} credentialsConfig := mockCredentialConfig{} - verifier := CredentialVerifier{did: "did:key:verifier", signingKey: testKey, tokenCache: &tokenCache, sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, validationServices: []ValidationService{&mockExternalSsiKit{tc.verificationResult, tc.verificationError}}, clock: mockClock{}, credentialsConfig: credentialsConfig} + verifier := CredentialVerifier{did: "did:key:verifier", signingKey: testKey, tokenCache: &tokenCache, sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, validationServices: []ValidationService{&mockExternalSsiKit{tc.verificationResult, tc.verificationError}}, clock: mockClock{}, credentialsConfig: credentialsConfig, clientIdentification: configModel.ClientIdentification{Id: "did:key:verifier"}} sameDeviceResponse, err := verifier.AuthenticationResponse(tc.requestedState, &tc.testVP) if err != tc.expectedError { @@ -488,11 +570,12 @@ func TestInitVerifier(t *testing.T) { } tests := []test{ - {"A verifier should be properly intantiated.", configModel.Configuration{Verifier: configModel.Verifier{Did: "did:key:verifier", TirAddress: "https://tir.org", ValidationMode: "none", SessionExpiry: 30, KeyAlgorithm: "RS256"}}, nil}, - {"Without a did, no verifier should be instantiated.", configModel.Configuration{Verifier: configModel.Verifier{TirAddress: "https://tir.org", ValidationMode: "none", SessionExpiry: 30, KeyAlgorithm: "RS256"}}, ErrorNoDID}, - {"Without a tir, no verifier should be instantiated.", configModel.Configuration{Verifier: configModel.Verifier{Did: "did:key:verifier", SessionExpiry: 30, ValidationMode: "none", KeyAlgorithm: "RS256"}}, ErrorNoTIR}, - {"Without a validationMode, no verifier should be instantiated.", configModel.Configuration{Verifier: configModel.Verifier{Did: "did:key:verifier", TirAddress: "https://tir.org", ValidationMode: "blub", SessionExpiry: 30, KeyAlgorithm: "RS256"}}, ErrorUnsupportedValidationMode}, - {"Without a valid key algorithm, no verifier should be instantiated.", configModel.Configuration{Verifier: configModel.Verifier{Did: "did:key:verifier", TirAddress: "https://tir.org", ValidationMode: "none", SessionExpiry: 30, KeyAlgorithm: "SomethingWeird"}}, ErrorUnsupportedKeyAlgorithm}, + {"A verifier should be properly intantiated.", configModel.Configuration{Verifier: configModel.Verifier{Did: "did:key:verifier", TirAddress: "https://tir.org", ValidationMode: "none", SessionExpiry: 30, KeyAlgorithm: "RS256", SupportedModes: []string{"urlEncoded"}}}, nil}, + {"Without a did, no verifier should be instantiated.", configModel.Configuration{Verifier: configModel.Verifier{TirAddress: "https://tir.org", ValidationMode: "none", SessionExpiry: 30, KeyAlgorithm: "RS256", SupportedModes: []string{"urlEncoded"}}}, ErrorNoDID}, + {"Without a tir, no verifier should be instantiated.", configModel.Configuration{Verifier: configModel.Verifier{Did: "did:key:verifier", SessionExpiry: 30, ValidationMode: "none", KeyAlgorithm: "RS256", SupportedModes: []string{"urlEncoded"}}}, ErrorNoTIR}, + {"Without a validationMode, no verifier should be instantiated.", configModel.Configuration{Verifier: configModel.Verifier{Did: "did:key:verifier", TirAddress: "https://tir.org", ValidationMode: "blub", SessionExpiry: 30, KeyAlgorithm: "RS256", SupportedModes: []string{"urlEncoded"}}}, ErrorUnsupportedValidationMode}, + {"Without a valid key algorithm, no verifier should be instantiated.", configModel.Configuration{Verifier: configModel.Verifier{Did: "did:key:verifier", TirAddress: "https://tir.org", ValidationMode: "none", SessionExpiry: 30, KeyAlgorithm: "SomethingWeird", SupportedModes: []string{"urlEncoded"}}}, ErrorUnsupportedKeyAlgorithm}, + {"Without supported modes, no verifier should be instantiated.", configModel.Configuration{Verifier: configModel.Verifier{Did: "did:key:verifier", TirAddress: "https://tir.org", ValidationMode: "none", SessionExpiry: 30, KeyAlgorithm: "RS256"}}, ErrorSupportedModesNotSet}, } for _, tc := range tests { @@ -536,7 +619,7 @@ func TestGetJWKS(t *testing.T) { for _, tc := range tests { t.Run(tc.testName, func(t *testing.T) { - testKey, _ := jwk.New(tc.key) + testKey, _ := jwk.Import(tc.key) verifier := CredentialVerifier{signingKey: testKey} jwks := verifier.GetJWKS() @@ -544,7 +627,7 @@ func TestGetJWKS(t *testing.T) { if jwks.Len() != 1 { t.Errorf("TestGetJWKS: Exactly the current signing key should be included.") } - returnedKey, _ := jwks.Get(0) + returnedKey, _ := jwks.Key(0) expectedKey, _ := testKey.PublicKey() // we compare the json-output to avoid address comparison instead of by-value. if logging.PrettyPrintObject(expectedKey) != logging.PrettyPrintObject(returnedKey) { @@ -564,11 +647,38 @@ type mockTokenSigner struct { signingError error } -func (mts mockTokenSigner) Sign(t jwt.Token, alg jwa.SignatureAlgorithm, key interface{}, options ...jwt.SignOption) ([]byte, error) { +func (mts mockTokenSigner) Sign(t jwt.Token, options ...jwt.SignOption) ([]byte, error) { if mts.signingError != nil { return []byte{}, mts.signingError } - return jwt.Sign(t, alg, key, options...) + return jwt.Sign(t, options...) +} + +// get the static key +func getECDSAKey() (key jwk.Key) { + + d := new(big.Int) + d.SetString("1234567890123456789012345678901234567890", 10) // example private scalar + + // Choose the curve + curve := elliptic.P256() + + // Derive the public key point (X, Y) + x, y := curve.ScalarBaseMult(d.Bytes()) + + // Construct the private key + priv := &ecdsa.PrivateKey{ + PublicKey: ecdsa.PublicKey{ + Curve: curve, + X: x, + Y: y, + }, + D: d, + } + + testKey, _ := jwk.Import(priv) + + return testKey } func TestGetToken(t *testing.T) { @@ -577,10 +687,7 @@ func TestGetToken(t *testing.T) { signingError := errors.New("signature_failure") - ecdsKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - testKey, _ := jwk.New(ecdsKey) - publicKey, _ := testKey.PublicKey() - + testKey := getECDSAKey() type test struct { testName string testCode string @@ -617,7 +724,7 @@ func TestGetToken(t *testing.T) { return } - returnedToken, err := jwt.Parse([]byte(jwtString), jwt.WithVerify(jwa.ES256, publicKey)) + returnedToken, err := jwt.Parse([]byte(jwtString), jwt.WithVerify(false), jwt.WithValidate(false)) if err != nil { t.Errorf("%s - No valid token signature. Err: %v", tc.testName, err) @@ -659,28 +766,22 @@ func tokenEquals(receivedToken, expectedToken string) bool { if err != nil { return false } - receivedTokenMap, err := parsedReceivedToken.AsMap(context.TODO()) - if err != nil { - return false - } - receivedTokenMap["kid"] = "" + parsedReceivedToken.Remove("kid") + parsedExpectedToken, err := jwt.ParseString(expectedToken) if err != nil { return false } - expectedTokenMap, err := parsedExpectedToken.AsMap(context.TODO()) - if err != nil { - return false - } - expectedTokenMap["kid"] = "" - return cmp.Equal(receivedTokenMap, expectedTokenMap) + parsedExpectedToken.Remove("kid") + + return cmp.Equal(parsedReceivedToken, parsedExpectedToken) } type openIdProviderMetadataTest struct { host string testName string serviceIdentifier string - credentialScopes map[string]map[string]map[string]configModel.Credential + credentialScopes map[string]map[string]config.ScopeEntry mockConfigError error expectedOpenID common.OpenIDProviderMetadata } @@ -690,12 +791,12 @@ func getOpenIdProviderMetadataTests() []openIdProviderMetadataTest { return []openIdProviderMetadataTest{ {testName: "Test OIDC metadata with existing scopes", serviceIdentifier: "serviceId", host: verifierHost, - credentialScopes: map[string]map[string]map[string]configModel.Credential{"serviceId": {"Scope1": {}, "Scope2": {}}}, mockConfigError: nil, + credentialScopes: map[string]map[string]config.ScopeEntry{"serviceId": {"Scope1": {}, "Scope2": {}}}, mockConfigError: nil, expectedOpenID: common.OpenIDProviderMetadata{ Issuer: verifierHost, ScopesSupported: []string{"Scope1", "Scope2"}}}, {testName: "Test OIDC metadata with non-existing scopes", serviceIdentifier: "serviceId", host: verifierHost, - credentialScopes: map[string]map[string]map[string]configModel.Credential{"serviceId": {}}, mockConfigError: nil, + credentialScopes: map[string]map[string]config.ScopeEntry{"serviceId": {}}, mockConfigError: nil, expectedOpenID: common.OpenIDProviderMetadata{ Issuer: verifierHost, ScopesSupported: []string{}}}, From e585024996e76ce98f4e77656b9b6d1c1c5ca173 Mon Sep 17 00:00:00 2001 From: Stefan Wiedemann Date: Fri, 2 May 2025 16:31:00 +0200 Subject: [PATCH 2/3] extend doc --- README.md | 162 ++++++++++++++++++++++++++++++++------ verifier/verifier_test.go | 8 +- 2 files changed, 143 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index c12b3e4a..271eab4b 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,18 @@ logging: verifier: # did to be used by the verifier. did: + # identification of the verifier in communication with wallets + clientIdentification: + # identification used by the verifier when requesting authorization. Can be a did, but also methods like x509_san_dns + id: + # path to the signing key(in pem format) for request object. Needs to correspond with the id + keyPath: + # algorithm to be used for signing the request. Needs to match the signing key + requestKeyAlgorithm: + # depending on the id type, the certificate chain needs to be included in the object(f.e. in case of x509_san_dns) + certificatePath: + # supported modes for requesting authentication. in case of byReference and byValue, the clientIdentification needs to be properly configured + supportedModes: ["urlEncoded", "byReference","byValue"] # address of the (ebsi-compliant) trusted-issuers-registry to be used for verifying the issuer of a received credential tirAddress: # Expiry(in seconds) of an authentication session. After that, a new flow needs to be initiated. @@ -118,6 +130,8 @@ verifier: # * `baseContext`: validates that only the fields and values (when applicable)are present in the document. No extra fields are allowed (outside of credentialSubject). # Default is set to `none` to ensure backwards compatibility validationMode: + # algorithm to be used for the jwt signatures - currently supported: RS256 and ES256, default is RS256 + keyAlgorithm: # configuration of the service to retrieve configuration for configRepo: @@ -126,28 +140,57 @@ configRepo: # static configuration for services services: # name of the service to be configured - testService: - # scope to be requested from the wallet - scope: - - VerifiableCredential - - CustomerCredential - # trusted participants endpoint configuration - trustedParticipants: - # the credentials type to configure the endpoint(s) for - VerifiableCredential: - - https://tir-pdc.ebsi.fiware.dev - # the credentials type to configure the endpoint(s) for - CustomerCredential: - - https://tir-pdc.ebsi.fiware.dev - # trusted issuers endpoint configuration - trustedIssuers: - # the credentials type to configure the endpoint(s) for - VerifiableCredential: - - https://tir-pdc.ebsi.fiware.dev - # the credentials type to configure the endpoint(s) for - CustomerCredential: - - https://tir-pdc.ebsi.fiware.dev - + - id: testService + # default scope for the service + defaultOidcScope: "default" + # the concrete scopes for the service, defining the trust for credentials and the presentation definition to be requested + oidcScopes: + # the concrete scope configuration + default: + # credentials and their trust configuration + credentials: + - type: CustomerCredential + # trusted participants endpoint configuration + trustedParticipantsLists: + # the credentials type to configure the endpoint(s) for + VerifiableCredential: + - https://tir-pdc.ebsi.fiware.dev + # the credentials type to configure the endpoint(s) for + CustomerCredential: + - https://tir-pdc.ebsi.fiware.dev + # trusted issuers endpoint configuration + trustedIssuersLists: + # the credentials type to configure the endpoint(s) for + VerifiableCredential: + - https://tir-pdc.ebsi.fiware.dev + # the credentials type to configure the endpoint(s) for + CustomerCredential: + - https://tir-pdc.ebsi.fiware.dev + # configuration for verifying the holder of a credential + holderVerification: + # should it be checked? + enabled: true + # claim to retrieve the holder from + claim: subject + # credentials and claims to be requested + presentationDefinition: + id: my-presentation + # List of requested inputs + input_descriptors: + id: my-descriptor + # defines the infromation to be requested + constraints: + # array of objects to describe the information to be included + fields: + - id: my-field + path: + - $.vct + filter: + const: "CustomerCredential" + # format of the credential to be requested + format: + 'sd+jwt-vc': + alg: ES256 ``` #### Templating @@ -165,6 +208,11 @@ In order to ease the integration into frontends, VCVerifier offers a login-page In order to start a ```same-device```-flow(e.g. the credential is hold by the requestor, instead of an additional device like a mobile wallet) call: ```shell + # scope to be requested from the wallet + scope: + - VerifiableCredential + - CustomerCredential + curl -X 'GET' \ 'http://localhost:8080/api/v1/samedevice?state=274e7465-cc9d-4cad-b75f-190db927e56a' ``` @@ -235,6 +283,11 @@ configRepo: # the credentials type to configure the endpoint(s) for VerifiableCredential: - type: ebsi + # scope to be requested from the wallet + scope: + - VerifiableCredential + - CustomerCredential + url: https://tir-pdc.ebsi.fiware.dev ``` @@ -258,7 +311,12 @@ configRepo: ### Gaia-X Registry -When using the [Gaia-X Digital Clearing House's](https://gaia-x.eu/services-deliverables/digital-clearing-house/) Registry Services, the issuer to be checked needs to fullfill the requirements of a Gaia-X participant. Thus, only did:web is supported for such and they need to provide a valid ```x5u``` location as part of their ```publicKeyJwk```. Usage of such registries can than be configured as following: +When using the [Gaia-X Digital Clearing House's](https://gaia-x.eu/services-deliverables/digital-clearing-house/) Registry Services, the issuer to be checked needs to fullfill the requirements of + # scope to be requested from the wallet + scope: + - VerifiableCredential + - CustomerCredential + a Gaia-X participant. Thus, only did:web is supported for such and they need to provide a valid ```x5u``` location as part of their ```publicKeyJwk```. Usage of such registries can than be configured as following: ```yaml configRepo: # static configuration for services @@ -298,6 +356,64 @@ configRepo: url: https://registry.lab.gaia-x.eu ``` +### Request modes + +In order to support various wallets, the verifier supports 3 modes of requesting authentication: +- Passing as URL with encoded parameters: "urlEncoded" +- Passing a request object as value: "byValue" +- Passing a request object by reference: "byReference" + +Following the [RFC9101](https://www.rfc-editor.org/rfc/rfc9101.html), in the second and third case the request is encoded as a signed JWT. Therefor ```clientIdentification``` for the verifier needs to be properly configured. + +The mode can be set during the intial requests, by sending the parameter "requestMode"(see [API Spec](./api/api.yaml)).Since requestObjects can become large and therefor also the QR-Codes generated out of them, the 3rd mode is recommended. + +#### urlEncoded + +Example: +``` + openid4vp://?response_type=vp_token&response_mode=direct_post&client_id=did:key:verifier&redirect_uri=https://verifier.org/api/v1/authentication_response&state=randomState&nonce=randomNonce +``` + +#### byValue +Example: +``` + openid4vp://?client_id=did:key:verifier&request=eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnZlcmlmaWVyIiwiZXhwIjozMCwiaXNzIjoiZGlkOmtleTp2ZXJpZmllciIsIm5vbmNlIjoicmFuZG9tTm9uY2UiLCJwcmVzZW50YXRpb25fZGVmaW5pdGlvbiI6eyJpZCI6IiIsImlucHV0X2Rlc2NyaXB0b3JzIjpudWxsLCJmb3JtYXQiOm51bGx9LCJyZWRpcmVjdF91cmkiOiJodHRwczovL3ZlcmlmaWVyLm9yZy9hcGkvdjEvYXV0aGVudGljYXRpb25fcmVzcG9uc2UiLCJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJzY29wZSI6Im9wZW5pZCIsInN0YXRlIjoicmFuZG9tU3RhdGUifQ.Z0xv_E9vvhRN2nBeKQ49LgH8lkjkX-weR7R5eCmX9ebGr1aE8_6usa2PO9nJ4LRv8oWMg0q9fsQ2x5DTYbvLdA +``` +Decoded: +```json +{ + "alg": "ES256", + "typ": "oauth-authz-req+jwt" +}. +{ + "client_id": "did:key:verifier", + "exp": 30, + "iss": "did:key:verifier", + "nonce": "randomNonce", + "presentation_definition": { + "id": "", + "input_descriptors": null, + "format": null + }, + "redirect_uri": "https://verifier.org/api/v1/authentication_response", + "response_type": "vp_token", + "scope": "openid", + "state": "randomState" +}. +signature +``` + +#### byReference + +Example: +``` + openid4vp://?client_id=did:key:verifier&request_uri=verifier.org/api/v1/request/randomState&request_uri_method=get" +``` +The object than can be retrived via: +```shell + curl https://verifier.org/api/v1/request/randomState +``` + ## API The API implements enpoints defined in [OIDC4VP](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-terminology) and [SIOP-2](https://openid.net/specs/openid-connect-self-issued-v2-1_0.html). The OpenAPI Specification of the implemented endpoints can be found at: [api/api.yaml](api/api.yaml). diff --git a/verifier/verifier_test.go b/verifier/verifier_test.go index 7754ae8f..3bd176cc 100644 --- a/verifier/verifier_test.go +++ b/verifier/verifier_test.go @@ -261,7 +261,7 @@ func TestStartSiopFlow(t *testing.T) { sessionCache := mockSessionCache{sessions: map[string]loginSession{}, errorToThrow: tc.sessionCacheError} nonceGenerator := mockNonceGenerator{staticValues: []string{"randomState", "randomNonce"}} credentialsConfig := mockCredentialConfig{tc.credentialScopes, tc.mockConfigError} - verifier := CredentialVerifier{did: "did:key:verifier", sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, tokenSigner: mockTokenSigner{}, clock: mockClock{}, requestSigningKey: &testKey, credentialsConfig: credentialsConfig, clientIdentification: configModel.ClientIdentification{Id: "did:key:verifier", KeyPath: "/my-signing-key.pem", KeyAlgorithm: "ES256"}} + verifier := CredentialVerifier{host: tc.testHost, did: "did:key:verifier", sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, tokenSigner: mockTokenSigner{}, clock: mockClock{}, requestSigningKey: &testKey, credentialsConfig: credentialsConfig, clientIdentification: configModel.ClientIdentification{Id: "did:key:verifier", KeyPath: "/my-signing-key.pem", KeyAlgorithm: "ES256"}} authReq, err := verifier.StartSiopFlow(tc.testHost, tc.testProtocol, tc.testAddress, tc.testSessionId, tc.testClientId, tc.requestMode) verifyInitTest(t, tc, authReq, err, sessionCache, false) }) @@ -332,7 +332,7 @@ func getInitSiopTests() []siopInitTest { expectedConnection: "openid4vp://?client_id=did:key:verifier&request=eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnZlcmlmaWVyIiwiZXhwIjozMCwiaXNzIjoiZGlkOmtleTp2ZXJpZmllciIsIm5vbmNlIjoicmFuZG9tTm9uY2UiLCJwcmVzZW50YXRpb25fZGVmaW5pdGlvbiI6eyJpZCI6IiIsImlucHV0X2Rlc2NyaXB0b3JzIjpudWxsLCJmb3JtYXQiOm51bGx9LCJyZWRpcmVjdF91cmkiOiJodHRwczovL3ZlcmlmaWVyLm9yZy9hcGkvdjEvYXV0aGVudGljYXRpb25fcmVzcG9uc2UiLCJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJzY29wZSI6Im9wZW5pZCIsInN0YXRlIjoicmFuZG9tU3RhdGUifQ", sessionCacheError: nil, expectedError: nil, }, {testName: "If all parameters are set, a proper connection string byReference should be returned.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "", requestMode: REQUEST_MODE_BY_REFERENCE, credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://client.org/callback", - expectedConnection: "openid4vp://?client_id=did:key:verifier&request_uri=/api/v1/request/randomState&request_uri_method=get", sessionCacheError: nil, expectedError: nil, testRequestObjectJwt: "eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnZlcmlmaWVyIiwiZXhwIjozMCwiaXNzIjoiZGlkOmtleTp2ZXJpZmllciIsIm5vbmNlIjoicmFuZG9tTm9uY2UiLCJwcmVzZW50YXRpb25fZGVmaW5pdGlvbiI6eyJpZCI6IiIsImlucHV0X2Rlc2NyaXB0b3JzIjpudWxsLCJmb3JtYXQiOm51bGx9LCJyZWRpcmVjdF91cmkiOiJodHRwczovL3ZlcmlmaWVyLm9yZy9hcGkvdjEvYXV0aGVudGljYXRpb25fcmVzcG9uc2UiLCJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJzY29wZSI6Im9wZW5pZCIsInN0YXRlIjoicmFuZG9tU3RhdGUifQ", + expectedConnection: "openid4vp://?client_id=did:key:verifier&request_uri=verifier.org/api/v1/request/randomState&request_uri_method=get", sessionCacheError: nil, expectedError: nil, testRequestObjectJwt: "eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnZlcmlmaWVyIiwiZXhwIjozMCwiaXNzIjoiZGlkOmtleTp2ZXJpZmllciIsIm5vbmNlIjoicmFuZG9tTm9uY2UiLCJwcmVzZW50YXRpb25fZGVmaW5pdGlvbiI6eyJpZCI6IiIsImlucHV0X2Rlc2NyaXB0b3JzIjpudWxsLCJmb3JtYXQiOm51bGx9LCJyZWRpcmVjdF91cmkiOiJodHRwczovL3ZlcmlmaWVyLm9yZy9hcGkvdjEvYXV0aGVudGljYXRpb25fcmVzcG9uc2UiLCJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJzY29wZSI6Im9wZW5pZCIsInN0YXRlIjoicmFuZG9tU3RhdGUifQ", }, } } @@ -358,7 +358,7 @@ func TestStartSameDeviceFlow(t *testing.T) { requestMode: REQUEST_MODE_BY_VALUE, expectedConnection: "https://verifier.org/redirect?client_id=did:key:verifier&request=eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnZlcmlmaWVyIiwiZXhwIjozMCwiaXNzIjoiZGlkOmtleTp2ZXJpZmllciIsIm5vbmNlIjoicmFuZG9tTm9uY2UiLCJwcmVzZW50YXRpb25fZGVmaW5pdGlvbiI6eyJpZCI6IiIsImlucHV0X2Rlc2NyaXB0b3JzIjpudWxsLCJmb3JtYXQiOm51bGx9LCJyZWRpcmVjdF91cmkiOiJodHRwczovL3ZlcmlmaWVyLm9yZy9hcGkvdjEvYXV0aGVudGljYXRpb25fcmVzcG9uc2UiLCJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJzY29wZSI6Im9wZW5pZCIsInN0YXRlIjoicmFuZG9tU3RhdGUifQ", sessionCacheError: nil, expectedError: nil, }, {testName: "If everything is provided, a samedevice flow should be started.", testHost: "verifier.org", testProtocol: "https", testAddress: "/redirect", testSessionId: "my-random-session-id", testClientId: "", credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://verifier.org/redirect", - requestMode: REQUEST_MODE_BY_REFERENCE, expectedConnection: "https://verifier.org/redirect?client_id=did:key:verifier&request_uri=/api/v1/request/randomState&request_uri_method=get", sessionCacheError: nil, expectedError: nil, testRequestObjectJwt: "eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnZlcmlmaWVyIiwiZXhwIjozMCwiaXNzIjoiZGlkOmtleTp2ZXJpZmllciIsIm5vbmNlIjoicmFuZG9tTm9uY2UiLCJwcmVzZW50YXRpb25fZGVmaW5pdGlvbiI6eyJpZCI6IiIsImlucHV0X2Rlc2NyaXB0b3JzIjpudWxsLCJmb3JtYXQiOm51bGx9LCJyZWRpcmVjdF91cmkiOiJodHRwczovL3ZlcmlmaWVyLm9yZy9hcGkvdjEvYXV0aGVudGljYXRpb25fcmVzcG9uc2UiLCJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJzY29wZSI6Im9wZW5pZCIsInN0YXRlIjoicmFuZG9tU3RhdGUifQ", + requestMode: REQUEST_MODE_BY_REFERENCE, expectedConnection: "https://verifier.org/redirect?client_id=did:key:verifier&request_uri=verifier.org/api/v1/request/randomState&request_uri_method=get", sessionCacheError: nil, expectedError: nil, testRequestObjectJwt: "eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnZlcmlmaWVyIiwiZXhwIjozMCwiaXNzIjoiZGlkOmtleTp2ZXJpZmllciIsIm5vbmNlIjoicmFuZG9tTm9uY2UiLCJwcmVzZW50YXRpb25fZGVmaW5pdGlvbiI6eyJpZCI6IiIsImlucHV0X2Rlc2NyaXB0b3JzIjpudWxsLCJmb3JtYXQiOm51bGx9LCJyZWRpcmVjdF91cmkiOiJodHRwczovL3ZlcmlmaWVyLm9yZy9hcGkvdjEvYXV0aGVudGljYXRpb25fcmVzcG9uc2UiLCJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJzY29wZSI6Im9wZW5pZCIsInN0YXRlIjoicmFuZG9tU3RhdGUifQ", }, } @@ -368,7 +368,7 @@ func TestStartSameDeviceFlow(t *testing.T) { sessionCache := mockSessionCache{sessions: map[string]loginSession{}, errorToThrow: tc.sessionCacheError} nonceGenerator := mockNonceGenerator{staticValues: []string{"randomState", "randomNonce"}} credentialsConfig := mockCredentialConfig{tc.credentialScopes, tc.mockConfigError} - verifier := CredentialVerifier{did: "did:key:verifier", sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, tokenSigner: mockTokenSigner{}, clock: mockClock{}, requestSigningKey: &testKey, credentialsConfig: credentialsConfig, clientIdentification: configModel.ClientIdentification{Id: "did:key:verifier", KeyPath: "/my-signing-key.pem", KeyAlgorithm: "ES256"}} + verifier := CredentialVerifier{host: tc.testHost, did: "did:key:verifier", sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, tokenSigner: mockTokenSigner{}, clock: mockClock{}, requestSigningKey: &testKey, credentialsConfig: credentialsConfig, clientIdentification: configModel.ClientIdentification{Id: "did:key:verifier", KeyPath: "/my-signing-key.pem", KeyAlgorithm: "ES256"}} authReq, err := verifier.StartSameDeviceFlow(tc.testHost, tc.testProtocol, tc.testSessionId, tc.testAddress, tc.testClientId, tc.requestMode) verifyInitTest(t, tc, authReq, err, sessionCache, true) }) From 27ff5d2f13c0e9ed692e9ff28b94baaf52351f90 Mon Sep 17 00:00:00 2001 From: Stefan Wiedemann Date: Fri, 2 May 2025 16:40:14 +0200 Subject: [PATCH 3/3] fix test --- verifier/verifier_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/verifier/verifier_test.go b/verifier/verifier_test.go index 3bd176cc..3ad7ccc0 100644 --- a/verifier/verifier_test.go +++ b/verifier/verifier_test.go @@ -233,6 +233,8 @@ func TestInitSiopFlow(t *testing.T) { logging.Configure(true, "DEBUG", true, []string{}) + testKey := getECDSAKey() + tests := getInitSiopTests() for _, tc := range tests { t.Run(tc.testName, func(t *testing.T) { @@ -240,8 +242,8 @@ func TestInitSiopFlow(t *testing.T) { sessionCache := mockSessionCache{sessions: map[string]loginSession{}, errorToThrow: tc.sessionCacheError} nonceGenerator := mockNonceGenerator{staticValues: []string{"randomState", "randomNonce"}} credentialsConfig := mockCredentialConfig{tc.credentialScopes, tc.mockConfigError} - verifier := CredentialVerifier{did: "did:key:verifier", sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, credentialsConfig: credentialsConfig, clientIdentification: configModel.ClientIdentification{Id: "did:key:verifier"}} - authReq, err := verifier.initSiopFlow(tc.testHost, tc.testProtocol, tc.testAddress, tc.testSessionId, tc.testClientId, "urlEncoded") + verifier := CredentialVerifier{host: tc.testHost, did: "did:key:verifier", sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, tokenSigner: mockTokenSigner{}, clock: mockClock{}, credentialsConfig: credentialsConfig, requestSigningKey: &testKey, clientIdentification: configModel.ClientIdentification{Id: "did:key:verifier", KeyPath: "/my-signing-key.pem", KeyAlgorithm: "ES256"}} + authReq, err := verifier.initSiopFlow(tc.testHost, tc.testProtocol, tc.testAddress, tc.testSessionId, tc.testClientId, tc.requestMode) verifyInitTest(t, tc, authReq, err, sessionCache, false) }) }