diff --git a/README.md b/README.md index e296086..8223fb5 100644 --- a/README.md +++ b/README.md @@ -111,18 +111,25 @@ 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 + # identification used by the verifier when requesting authorization, following the OIDC4VP client identifier prefixes(see https://openid.net/specs/openid-4-verifiable-presentations-1_0.html). Can be a did (e.g. "did:web:..."), an x509_san_dns entry ("x509_san_dns:") or a redirect_uri entry ("redirect_uri:"). redirect_uri only works with the "urlEncoded" request mode, see "Request modes" below - it is the only scheme whose requests must NOT be signed. If left empty, the "urlEncoded" mode defaults it per-request to "redirect_uri:" followed by the verifier's own callback URL, so it always matches. id: - # path to the signing key(in pem format) for request object. Needs to correspond with the id + # path to the signing key(in pem format) for request object. Needs to correspond with the id. Not used (and not needed) for the "redirect_uri" id scheme, since that scheme is never signed. keyPath: - # algorithm to be used for signing the request. Needs to match the signing key + # algorithm to be used for signing the request. Needs to match the signing key. Not used for the "redirect_uri" id scheme. requestKeyAlgorithm: - # depending on the id type, the certificate chain needs to be included in the object(f.e. in case of x509_san_dns) + # depending on the id type, the certificate chain needs to be included in the object(f.e. in case of x509_san_dns). Not used for the "redirect_uri" id scheme. certificatePath: - # Kid used when key certificate does not include it. If both are missing, id is used + # Kid used when key certificate does not include it. If both are missing, id is used. Not used for the "redirect_uri" id scheme. kid: - # supported modes for requesting authentication. in case of byReference and byValue, the clientIdentification needs to be properly configured + # supported modes for requesting authentication. in case of byReference and byValue, the clientIdentification needs to be properly configured(signing key required). urlEncoded is the only mode that does not sign the request, and the only one compatible with the "redirect_uri" id scheme above. supportedModes: ["urlEncoded", "byReference","byValue"] + # request mode used for flows where the caller has no way to request one explicitly - the + # OIDC-bridging authorization endpoint(/api/v1/authorization), and /api/v2/loginQR when no + # request_mode query parameter is given. Must be one of supportedModes above. Defaults to + # "byReference" for backwards compatibility, but only when "byReference" is itself listed in + # supportedModes - otherwise it defaults to the first entry of supportedModes instead, so + # e.g. a supportedModes: ["urlEncoded"]-only config keeps working without setting this. + requestMode: byReference # 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. @@ -578,15 +585,28 @@ The mode can be set during the intial requests, by sending the parameter "reques #### urlEncoded +The only mode where the request is **not** signed — every parameter (including `presentation_definition`/`dcql_query`, when configured) is inlined as a plain query parameter instead of being wrapped in a JWT. Because nothing is fetched separately and nothing is base64-encoded into a compact JWT, the resulting URI/QR-code is larger than with `byValue`/`byReference`. + +This is the **only mode compatible with the `redirect_uri` [client identifier prefix](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html)** (as opposed to e.g. `did:...` or `x509_san_dns:...`). Per the OIDC4VP spec, requests using `redirect_uri` as client identifier scheme cannot be signed, since there is no key/certificate for the wallet to verify a signature against — the wallet's only trust check is that the response is sent back to the exact URI embedded in `client_id`. + +To use it: +* leave `clientIdentification.id` empty and the verifier defaults it, per request, to `redirect_uri:` followed by its own callback URL (`/api/v1/authentication_response`) — this is always consistent, since both values are derived from the same incoming request. +* alternatively, set `clientIdentification.id` explicitly to `redirect_uri:` followed by the verifier's own callback URL, exactly as it will be sent as `response_uri`, e.g. `redirect_uri:https://verifier.org/api/v1/authentication_response`. If the host in `id` ever stops matching the host of the incoming request (e.g. after an ingress hostname or `pathPrefix` change), the verifier rejects the request instead of sending a `client_id`/`response_uri` pair the wallet is guaranteed to reject. +* `keyPath`, `requestKeyAlgorithm` and `certificatePath` are **not needed** for this mode (no signing, no `x5c` header) — they only matter if `byValue`/`byReference` are also listed in `supportedModes` for other wallets. +* make sure `"urlEncoded"` is included in `supportedModes`, and either pass `requestMode=urlEncoded` on the initial request, or set it as the default (see below). +* if `"urlEncoded"` is the **only** entry in `supportedModes`, `verifier.requestMode` defaults to it automatically (the built-in `byReference` default only applies when it is itself listed in `supportedModes`) — you only need to set `verifier.requestMode` explicitly if you list several modes and want a non-`byReference` default. + +Trade-off: `redirect_uri` gives up the cryptographic proof of the verifier's identity that `did:...`/`x509_san_dns:...` provide — use it only when the wallet you need to support doesn't implement a signed scheme (some do not, see wallet compatibility notes below), or as a fallback for wallets you don't fully control. + 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 + openid4vp://?response_type=vp_token&response_mode=direct_post&client_id=redirect_uri:https://verifier.org/api/v1/authentication_response&response_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 + openid4vp://?client_id=did:key:verifier&request=eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnZlcmlmaWVyIiwiZXhwIjozMCwiaXNzIjoiZGlkOmtleTp2ZXJpZmllciIsIm5vbmNlIjoicmFuZG9tTm9uY2UiLCJwcmVzZW50YXRpb25fZGVmaW5pdGlvbiI6eyJpZCI6IiIsImlucHV0X2Rlc2NyaXB0b3JzIjpudWxsLCJmb3JtYXQiOm51bGx9LCJyZXNwb25zZV91cmkiOiJodHRwczovL3ZlcmlmaWVyLm9yZy9hcGkvdjEvYXV0aGVudGljYXRpb25fcmVzcG9uc2UiLCJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJzY29wZSI6Im9wZW5pZCIsInN0YXRlIjoicmFuZG9tU3RhdGUifQ.Z0xv_E9vvhRN2nBeKQ49LgH8lkjkX-weR7R5eCmX9ebGr1aE8_6usa2PO9nJ4LRv8oWMg0q9fsQ2x5DTYbvLdA ``` Decoded: ```json @@ -604,7 +624,7 @@ Decoded: "input_descriptors": null, "format": null }, - "redirect_uri": "https://verifier.org/api/v1/authbyValentication_response", + "response_uri": "https://verifier.org/api/v1/authentication_response", "response_type": "vp_token", "scope": "openid", "state": "randomState" diff --git a/config/config.go b/config/config.go index 0280db1..48cb4e3 100644 --- a/config/config.go +++ b/config/config.go @@ -176,6 +176,11 @@ type Verifier struct { ClientIdentification ClientIdentification `mapstructure:"clientIdentification"` // supported request modes - currently 'urlEncoded', 'byValue' and 'byReference' are available. In case of byValue, the keyPath has to be set. SupportedModes []string `mapstructure:"supportedModes" default:"urlEncoded"` + // request mode to use for flows where the caller has no way to request one explicitly + // (e.g. the OIDC-bridging /api/v1/authorization endpoint, or /api/v2/loginQR when no + // request_mode query parameter is given). Must be one of supportedModes when set. Defaults + // to "byReference" to preserve the pre-existing, hardcoded behaviour. + RequestMode string `mapstructure:"requestMode" default:"byReference"` // address of the (ebsi-compatible) trusted-issuers-registry for verifying the issuer TirAddress string `mapstructure:"tirAddress"` // expiry of the tir-cache entries diff --git a/config/configClient.go b/config/configClient.go index f6cc768..d0369d6 100644 --- a/config/configClient.go +++ b/config/configClient.go @@ -518,7 +518,7 @@ type DCQL struct { // A non-empty array of Credential Queries that specify the requested Credentials. Credentials []CredentialQuery `json:"credentials" mapstructure:"credentials"` // A non-empty array of Credential Set Queries that specifies additional constraints on which of the requested Credentials to return. - CredentialSets []CredentialSetQuery `json:"credential_sets" mapstructure:"credential_sets"` + CredentialSets []CredentialSetQuery `json:"credential_sets,omitempty" mapstructure:"credential_sets,omitempty"` } // CredentialQuery is an object representing a request for a presentation of one or more matching Credentials @@ -538,7 +538,7 @@ type CredentialQuery struct { // A non-empty array containing arrays of identifiers for elements in claims that specifies which combinations of claims for the Credential are requested. ClaimSets [][]string `json:"claim_sets,omitempty" mapstructure:"claim_sets,omitempty"` // A non-empty array of objects that specifies expected authorities or trust frameworks that certify Issuers, that the Verifier will accept. Every Credential returned by the Wallet SHOULD match at least one of the conditions present in the corresponding trusted_authorities array if present. - TrustedAuthorities []TrustedAuthorityQuery `json:"trusted_authorities" mapstructure:"trusted_authorities"` + TrustedAuthorities []TrustedAuthorityQuery `json:"trusted_authorities,omitempty" mapstructure:"trusted_authorities,omitempty"` } // RequiresCryptographicHolderBinding returns true when the field is nil (absent) or explicitly true. diff --git a/config/configClient_test.go b/config/configClient_test.go index 936c901..b435208 100644 --- a/config/configClient_test.go +++ b/config/configClient_test.go @@ -485,3 +485,22 @@ func TestTrustedIssuersListsDecodeHook(t *testing.T) { assert.Equal(t, input, result) }) } + +func Test_DCQL_MarshalJSON_OmitsUnsetOptionalArrays(t *testing.T) { + dcql := DCQL{Credentials: []CredentialQuery{{Id: "my-cred"}}} + + out, err := json.Marshal(dcql) + assert.NoError(t, err) + assert.NotContains(t, string(out), "credential_sets") + assert.NotContains(t, string(out), "trusted_authorities") + + var roundTripped map[string]interface{} + assert.NoError(t, json.Unmarshal(out, &roundTripped)) + _, hasCredentialSets := roundTripped["credential_sets"] + assert.False(t, hasCredentialSets) + + credentials := roundTripped["credentials"].([]interface{}) + credential := credentials[0].(map[string]interface{}) + _, hasTrustedAuthorities := credential["trusted_authorities"] + assert.False(t, hasTrustedAuthorities) +} diff --git a/config/provider_test.go b/config/provider_test.go index e8e507e..41b8389 100644 --- a/config/provider_test.go +++ b/config/provider_test.go @@ -56,6 +56,7 @@ func Test_ReadConfig(t *testing.T) { KeyAlgorithm: "RS256", GenerateKey: true, SupportedModes: []string{"urlEncoded"}, + RequestMode: "byReference", JwtExpiration: 30, StatusListCacheExpiry: DefaultStatusCacheExpirySeconds, StatusListHttpTimeout: DefaultStatusHttpTimeoutSeconds, @@ -145,6 +146,7 @@ func Test_ReadConfig(t *testing.T) { KeyAlgorithm: "RS256", GenerateKey: true, SupportedModes: []string{"urlEncoded"}, + RequestMode: "byReference", JwtExpiration: 30, StatusListCacheExpiry: DefaultStatusCacheExpirySeconds, StatusListHttpTimeout: DefaultStatusHttpTimeoutSeconds, @@ -198,6 +200,7 @@ func Test_ReadConfig(t *testing.T) { KeyAlgorithm: "RS256", GenerateKey: true, SupportedModes: []string{"urlEncoded"}, + RequestMode: "byReference", JwtExpiration: 30, StatusListCacheExpiry: DefaultStatusCacheExpirySeconds, StatusListHttpTimeout: DefaultStatusHttpTimeoutSeconds, diff --git a/go.sum b/go.sum index d1654b4..c959213 100644 --- a/go.sum +++ b/go.sum @@ -34,6 +34,7 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= @@ -44,6 +45,7 @@ github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk= github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -52,6 +54,7 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -60,6 +63,7 @@ github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edY github.com/bits-and-blooms/bitset v1.7.0 h1:YjAGVd3XmtK9ktAbX8Zg2g2PwLIMjGREZJHlV4j7NEo= github.com/bits-and-blooms/bitset v1.7.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= @@ -82,11 +86,13 @@ github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CL 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.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 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= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -102,6 +108,7 @@ github.com/foolin/goview v0.3.0 h1:q5wKwXKEFb20dMRfYd59uj5qGCo7q4L9eVHHUjmMWrg= github.com/foolin/goview v0.3.0/go.mod h1:OC1VHC4FfpWymhShj8L1Tc3qipFmrmm+luAEdTvkos4= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/getkin/kin-openapi v0.107.0/go.mod h1:9Dhr+FasATJZjS4iOLvB0hkaxgYdulrNYm2e9epLWOo= github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g= github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs= github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= @@ -112,6 +119,7 @@ github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjX github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -123,6 +131,10 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= @@ -138,6 +150,11 @@ github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k= github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-redis/redis/v9 v9.0.0-beta.2/go.mod h1:Bldcd/M/bm9HbnNPi/LUtYBSD8ttcZYBMupwMXhdU0o= +github.com/go-rel/mysql v0.9.1/go.mod h1:gjpOe9PNvAnkmK7EP1Ne3gbrwkRSQ/VUx8d5BHrXWaU= +github.com/go-rel/rel v0.39.0/go.mod h1:yN6+aimHyRIzbuWFe5DaxiZPuVuPfd7GlLpy/YTqTUg= +github.com/go-rel/reltest v0.11.0/go.mod h1:NWpBpRcdzy7UU6/KZtJVLOvCKoiNcQEWYEZ9//cCaTw= +github.com/go-rel/sql v0.13.0/go.mod h1:kAxb7ARO4vokQB+2ot4rNT1mhNzd0ofHp3oihvSw1oU= github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU= github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -147,6 +164,8 @@ github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PU 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= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -177,6 +196,8 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -216,6 +237,8 @@ github.com/gookit/goutil v0.6.6 h1:XdvnPocHpKDXA+eykfc/F846Y1V2Vyo3+cV8rfliG90= github.com/gookit/goutil v0.6.6/go.mod h1:D++7kbQd/6vECyYTxB5tq6AKDIG9ZYwZNhubWJvN9dw= github.com/gookit/ini/v2 v2.2.1 h1:6fCrz8icnUHhYqGZwu7RtHLh+v+ErrgrAt9+aIcoJCc= github.com/gookit/ini/v2 v2.2.1/go.mod h1:as7p0WsJg4sufGECEqNdY/+I026qRnyzPa4UYHuuOR4= +github.com/gookit/properties v0.3.0/go.mod h1:020VQRBo8R5gJZaMc+ohmLmUv4esuv5xw3/zNJYvxuE= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -223,20 +246,33 @@ github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/hcl/v2 v2.16.1/go.mod h1:JRmR89jycNkrrqnMmvPDMd56n1rQJ2Q6KocSLCMCXng= github.com/hellofresh/health-go/v5 v5.0.0 h1:jxjllHekqEU4VYIajKJtFoOxDp1YaaygNWwAoZwWFh0= github.com/hellofresh/health-go/v5 v5.0.0/go.mod h1:9hFVIBdKkxrg1bJurUPlw1D/0FWhl47IVfGYPy4Op9o= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/influxdata/influxdb-client-go/v2 v2.9.0/go.mod h1:x7Jo5UHHl+w8wu8UnGiNobDDHygojXwJX4mx7rXGKMk= +github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v1.12.1/go.mod h1:ZkhRC59Llhrq3oSfrikvwQ5NaxYExr6twkdkMLaKono= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3/v2 v2.3.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgtype v1.11.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ= github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -250,6 +286,7 @@ github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPci github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= @@ -267,21 +304,31 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= github.com/labstack/echo/v4 v4.1.6/go.mod h1:kU/7PwzgNxZH4das4XNsSpBSOD09XIF5YEPzjpkGnGE= +github.com/labstack/echo/v4 v4.9.1/go.mod h1:Pop5HLc+xoc4qhTZ1ip6C0RtP7Z+4VzRLWZZFKqbbjo= github.com/labstack/gommon v0.2.9/go.mod h1:E8ZTmW9vw5az5/ZyHWCp0Lw4OH2ecsaBP1C/NKavGG4= +github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 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/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= 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/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= 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/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx v1.2.25/go.mod h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY= +github.com/lestrrat-go/jwx/v2 v2.0.8/go.mod h1:zLxnyv9rTlEvOUHbc48FAfIL8iYu2hHvIRaTFGc8mT0= 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/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matryer/moq v0.2.7/go.mod h1:kITsx543GOENm48TUAQyJ9+SAvFSr7iGQXPoth/VUBk= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -295,6 +342,7 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -304,6 +352,7 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= @@ -317,6 +366,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= @@ -364,6 +415,7 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/rabbitmq/amqp091-go v1.3.4/go.mod h1:ogQDLSOACsLPsIq0NpbtiifNZi2YOz0VTJ0kHRghqbM= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -374,9 +426,11 @@ github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDN 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/serenize/snaker v0.0.0-20201027110005-a7ad2135616e/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs= 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= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= @@ -413,12 +467,20 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/vitorsalgado/mocha/v2 v2.0.2/go.mod h1:l7jRVm7KTL4VAxxazH99UVo+KzwztjrYpFTksTmL1DE= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= +github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yosuke-furukawa/json5 v0.1.1/go.mod h1:sw49aWDqNdRJ6DYUtIQiaA3xyj2IL9tjeNYmX2ixwcU= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zclconf/go-cty v1.12.1/go.mod h1:s9IfD1LK5ccNMSWCVFCE2rJfHiZgi7JijgeWIMfhLvA= +go.mongodb.org/mongo-driver v1.9.1/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -600,6 +662,7 @@ 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= +golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -712,6 +775,7 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/openapi/api_api.go b/openapi/api_api.go index a24250a..e19d01b 100644 --- a/openapi/api_api.go +++ b/openapi/api_api.go @@ -230,25 +230,27 @@ func AuthorizationEndpoint(c *gin.Context) { } authorizationType := getApiVerifier().GetAuthorizationType(clientId) + fallbackRequestMode := getApiVerifier().GetRequestMode() var redirect string var err error switch authorizationType { case DEEPLINK: - redirect, err = getApiVerifier().StartSameDeviceFlow(c.Request.Host, protocol, state, "", clientId, nonce, verifier.REQUEST_MODE_BY_REFERENCE, scope, verifier.OPENID4VP_PROTOCOL) + redirect, err = getApiVerifier().StartSameDeviceFlow(c.Request.Host, protocol, state, "", clientId, nonce, fallbackRequestMode, scope, verifier.OPENID4VP_PROTOCOL) if err != nil { logging.Log().Warnf("Was not able start a same device flow. Err: %v", err) c.AbortWithStatusJSON(http.StatusInternalServerError, ErrorMessageFailedSameDevice) return } case FRONTEND_V2: - redirect = buildFrontendV2Address(protocol, c.Request.Host, getApiVerifier().GetPathPrefix(), state, clientId, redirectUri, scope, nonce) + redirect = buildFrontendV2Address(protocol, c.Request.Host, getApiVerifier().GetPathPrefix(), state, clientId, redirectUri, scope, nonce, fallbackRequestMode) } c.Redirect(http.StatusFound, redirect) } -func buildFrontendV2Address(protocol, host, prefix, state, clientId, redirectUri, scope, nonce string) string { - logging.Log().Debugf("%s://%s%s/api/v2/loginQR?state=%s&client_id=%s&redirect_uri=%s&scope=%s&nonce=%s&request_mode=byReference", protocol, host, prefix, state, clientId, redirectUri, scope, nonce) - return fmt.Sprintf("%s://%s%s/api/v2/loginQR?state=%s&client_id=%s&redirect_uri=%s&scope=%s&nonce=%s&request_mode=byReference", protocol, host, prefix, state, clientId, redirectUri, scope, nonce) +func buildFrontendV2Address(protocol, host, prefix, state, clientId, redirectUri, scope, nonce, requestMode string) string { + address := fmt.Sprintf("%s://%s%s/api/v2/loginQR?state=%s&client_id=%s&redirect_uri=%s&scope=%s&nonce=%s&request_mode=%s", protocol, host, prefix, state, clientId, redirectUri, scope, nonce, requestMode) + logging.Log().Debugf("%s", address) + return address } // GetToken - Token endpoint to exchange the authorization code with the actual JWT. @@ -531,8 +533,9 @@ func StartSIOPSameDevice(c *gin.Context) { requestMode, requestModeExists := c.GetQuery("request_mode") if !requestModeExists { - logging.Log().Infof("Using default request mode %s.", DEFAULT_REQUEST_MODE) - requestMode = DEFAULT_REQUEST_MODE + fallbackRequestMode := getApiVerifier().GetRequestMode() + logging.Log().Infof("Using default request mode %s.", fallbackRequestMode) + requestMode = fallbackRequestMode } scope, scopeExists := c.GetQuery("scope") @@ -846,8 +849,9 @@ func VerifierAPIStartSIOP(c *gin.Context) { requestMode, requestModeExists := c.GetQuery("request_mode") if !requestModeExists { - logging.Log().Infof("Using default request mode %s.", DEFAULT_REQUEST_MODE) - requestMode = DEFAULT_REQUEST_MODE + fallbackRequestMode := getApiVerifier().GetRequestMode() + logging.Log().Infof("Using default request mode %s.", fallbackRequestMode) + requestMode = fallbackRequestMode } connectionString, err := getApiVerifier().StartSiopFlow(c.Request.Host, protocol, callback, state, clientId, "", requestMode) diff --git a/openapi/api_api_test.go b/openapi/api_api_test.go index cd2a1c5..b3e3971 100644 --- a/openapi/api_api_test.go +++ b/openapi/api_api_test.go @@ -54,6 +54,7 @@ type mockVerifier struct { mockExchangeRefresh string mockExchangeError error mockPathPrefix string + mockRequestMode string } func (mV *mockVerifier) ReturnLoginQR(host string, protocol string, callback string, sessionId string, clientId string, nonce string, requestType string) (qr string, err error) { @@ -96,6 +97,10 @@ func (mV *mockVerifier) GetPathPrefix() string { return mV.mockPathPrefix } +func (mV *mockVerifier) GetRequestMode() string { + return mV.mockRequestMode +} + // TODO func (mV *mockVerifier) GetRequestObject(state string) (jwt string, err error) { return jwt, err @@ -734,6 +739,7 @@ func buildSignedVPToken(t *testing.T) string { // TestGetPresentationFromQuery exercises both shapes of the DCQL vp_token map: // - OID4VP drafts 22-24: {"": ""} // - OID4VP draft 25+: {"": ["", ...]} +// // Both must be accepted (backward-compatible). Inputs that are neither a query // map nor parseable as a single token must return (nil, nil) so the caller // falls through to the flat-string presentation parsing path. A token that is @@ -828,7 +834,7 @@ func TestBuildFrontendV2Address(t *testing.T) { } for _, tc := range tests { t.Run(tc.testName, func(t *testing.T) { - actual := buildFrontendV2Address("https", "verifier.org", tc.prefix, "my-state", "my-client", "https://wallet.example/callback", "", "my-nonce") + actual := buildFrontendV2Address("https", "verifier.org", tc.prefix, "my-state", "my-client", "https://wallet.example/callback", "", "my-nonce", "byReference") if actual != tc.want { t.Errorf("%s - Expected %s but was %s", tc.testName, tc.want, actual) } @@ -836,6 +842,14 @@ func TestBuildFrontendV2Address(t *testing.T) { } } +func TestBuildFrontendV2Address_UsesProvidedRequestMode(t *testing.T) { + actual := buildFrontendV2Address("https", "verifier.org", "", "my-state", "my-client", "https://wallet.example/callback", "", "my-nonce", "urlEncoded") + want := "https://verifier.org/api/v2/loginQR?state=my-state&client_id=my-client&redirect_uri=https://wallet.example/callback&scope=&nonce=my-nonce&request_mode=urlEncoded" + if actual != want { + t.Errorf("Expected %s but was %s", want, actual) + } +} + // withBase64Padding appends json-insignificant whitespace until the length is not a multiple of 3, so // that encoding the result is guaranteed to emit '=' padding regardless of the fixture that is used. func withBase64Padding(raw []byte) []byte { diff --git a/openapi/api_frontend.go b/openapi/api_frontend.go index 43b27f2..9dd4efe 100644 --- a/openapi/api_frontend.go +++ b/openapi/api_frontend.go @@ -21,8 +21,6 @@ import ( "github.com/gin-gonic/gin" ) -const DEFAULT_REQUEST_MODE = verifier.REQUEST_MODE_BY_REFERENCE - var frontendVerifier verifier.Verifier var requestObjectClient *verifier.RequestObjectClient @@ -69,8 +67,9 @@ func VerifierPageDisplayQRSIOP(c *gin.Context) { requestMode, requestModeExists := c.GetQuery("request_mode") if !requestModeExists { - logging.Log().Infof("Using default request mode %s.", DEFAULT_REQUEST_MODE) - requestMode = DEFAULT_REQUEST_MODE + fallbackRequestMode := getFrontendVerifier().GetRequestMode() + logging.Log().Infof("Using default request mode %s.", fallbackRequestMode) + requestMode = fallbackRequestMode } qr, err := getFrontendVerifier().ReturnLoginQR(c.Request.Host, "https", callback, state, clientId, nonce, requestMode) @@ -137,8 +136,9 @@ func VerifierLoginQr(c *gin.Context) { requestMode, requestModeExists := c.GetQuery("request_mode") if !requestModeExists { - logging.Log().Infof("Using default request mode %s.", DEFAULT_REQUEST_MODE) - requestMode = DEFAULT_REQUEST_MODE + fallbackRequestMode := getFrontendVerifier().GetRequestMode() + logging.Log().Infof("Using default request mode %s.", fallbackRequestMode) + requestMode = fallbackRequestMode } qrInfo, err := getFrontendVerifier().ReturnLoginQRV2(c.Request.Host, "https", redirectUri, state, clientId, scope, nonce, requestMode) diff --git a/verifier/verifier.go b/verifier/verifier.go index 3dd9fca..631c824 100644 --- a/verifier/verifier.go +++ b/verifier/verifier.go @@ -14,6 +14,7 @@ import ( "fmt" "io" "net/http" + "net/url" "strings" "time" @@ -42,9 +43,16 @@ import ( "github.com/valyala/fasttemplate" ) +const REQUEST_MODE_URL_ENCODED = "urlEncoded" const REQUEST_MODE_BY_VALUE = "byValue" const REQUEST_MODE_BY_REFERENCE = "byReference" const REQUEST_OBJECT_TYP = "oauth-authz-req+jwt" + +// qrCodePixelsPerModule is the fixed size (in pixels) of a single QR code "module". +// Passed as a negative size to qrcode.Encode, it makes the rendered image grow with +// the content instead of scaling a fixed canvas, keeping modules scannable regardless +// of how large the encoded authentication request is. +const qrCodePixelsPerModule = 6 const ( CROSS_DEVICE_V1 = iota CROSS_DEVICE_V2 @@ -54,6 +62,10 @@ const ( const OPENID4VP_PROTOCOL = "openid4vp" const REDIRECT_PROTOCOL = "redirect" +// CLIENT_ID_REDIRECT_URI_PREFIX is the OIDC4VP client_id prefix for the "redirect_uri" +// client identifier scheme (https://openid.net/specs/openid-4-verifiable-presentations-1_0.html). +const CLIENT_ID_REDIRECT_URI_PREFIX = "redirect_uri:" + const DEFAULT_AUTHORIZATION_PATH = "/api/v1/authorization" const DEFAULT_SERIVCE_AUTHORIZATION_TYPE = "FRONTEND_V2" @@ -70,11 +82,13 @@ var ErrorNoSuchSession = errors.New("no_such_session") var ErrorWrongGrantType = errors.New("wrong_grant_type") var ErrorNoSuchCode = errors.New("no_such_code") var ErrorRedirectUriMismatch = errors.New("redirect_uri_does_not_match") +var ErrorClientIdHostMismatch = errors.New("client_id_host_does_not_match_response_uri") 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 ErrorNoValidCredentialTypeProvided = errors.New("no_valid_credential_type_provided") var ErrorUnsupportedRequestMode = errors.New("unsupported_request_mode") +var ErrorRequestModeNotSupported = errors.New("request_mode_not_in_supported_modes") var ErrorNoExpiration = errors.New("no_jwt_expiration_set") var ErrorNoKeyId = errors.New("no_key_id_available") var ErrorNoRequestObject = errors.New("no_request_object_available") @@ -112,6 +126,9 @@ type Verifier interface { GetRequestObject(state string) (jwt string, err error) GetHost() string GetPathPrefix() string + // GetRequestMode returns the request mode to use for flows where the caller has + // no way to request one explicitly (e.g. the OIDC-bridging authorization endpoint). + GetRequestMode() string GetAuthorizationType(clientId string) string GetDefaultScope(serviceIdentifier string) (string, error) // ExchangeRefreshToken atomically consumes a refresh token and returns a @@ -162,6 +179,8 @@ type CredentialVerifier struct { signingAlgorithm string // request modes supported by this instance of the verifier supportedRequestModes []string + // request mode used for flows where the caller has no way to request one explicitly + fallbackRequestMode string // Key for signing the request objects requestSigningKey *jwk.Key // Client identification for signing the request objects @@ -424,6 +443,7 @@ func InitVerifier(config *configModel.Configuration, repo database.ServiceReposi }, signingAlgorithm: verifierConfig.KeyAlgorithm, supportedRequestModes: verifierConfig.SupportedModes, + fallbackRequestMode: verifierConfig.RequestMode, requestSigningKey: &didSigningKey, clientIdentification: verifierConfig.ClientIdentification, verifierConfig: *verifierConfig, @@ -474,7 +494,12 @@ func (v *CredentialVerifier) ReturnLoginQR(host string, protocol string, callbac return qr, err } - png, err := qrcode.Encode(authenticationRequest, qrcode.Medium, 256) + // A negative size tells go-qrcode to render at a fixed number of pixels per module + // instead of scaling a fixed canvas, so the image grows with the content instead of + // the modules shrinking below what a phone camera can resolve (relevant for + // REQUEST_MODE_URL_ENCODED, whose inlined presentation_definition/dcql_query can push + // the QR to a much higher version than byValue/byReference ever need). + png, err := qrcode.Encode(authenticationRequest, qrcode.Medium, -qrCodePixelsPerModule) base64Img := base64.StdEncoding.EncodeToString(png) base64Img = "data:image/png;base64," + base64Img @@ -502,7 +527,12 @@ func (v *CredentialVerifier) ReturnLoginQRV2(host string, protocol string, redir return qrInfo, err } - png, err := qrcode.Encode(authenticationRequest, qrcode.Medium, 256) + // A negative size tells go-qrcode to render at a fixed number of pixels per module + // instead of scaling a fixed canvas, so the image grows with the content instead of + // the modules shrinking below what a phone camera can resolve (relevant for + // REQUEST_MODE_URL_ENCODED, whose inlined presentation_definition/dcql_query can push + // the QR to a much higher version than byValue/byReference ever need). + png, err := qrcode.Encode(authenticationRequest, qrcode.Medium, -qrCodePixelsPerModule) base64Img := base64.StdEncoding.EncodeToString(png) base64Img = "data:image/png;base64," + base64Img @@ -1374,6 +1404,14 @@ func (v *CredentialVerifier) initSiopFlow(host string, protocol string, callback func (v *CredentialVerifier) generateAuthenticationRequest(base string, clientId string, scope string, redirectUri string, state string, nonce string, loginSession loginSession, requestMode string) (authenticationRequest string, err error) { switch requestMode { + case REQUEST_MODE_URL_ENCODED: + authenticationRequest, err = v.createAuthenticationRequestUrlEncoded(base, redirectUri, state, clientId, scope, nonce) + if err != nil { + logging.Log().Warnf("Was not able to create the url-encoded authentication request. Error: %v", err) + } else { + logging.Log().Debugf("Authentication request is %s.", authenticationRequest) + } + return authenticationRequest, err case REQUEST_MODE_BY_VALUE: authenticationRequest, err = v.createAuthenticationRequestByValue(base, redirectUri, state, clientId, scope, nonce) if err != nil { @@ -1549,6 +1587,91 @@ func (v *CredentialVerifier) createAuthenticationRequestObject(response_uri stri } // creates an authenticationRequest string from the given parameters +// createAuthenticationRequestUrlEncoded builds an unsigned authentication request, with all +// parameters(including presentation_definition/dcql_query) inlined as plain query parameters +// on the request URI, instead of wrapped in a signed JWT. This is the only valid transport for +// the "redirect_uri" client_id scheme, since that scheme cannot be used with signed requests +// (there is no certificate/DID for the wallet to verify the signature against). It produces a +// larger QR/URI than "byValue"/"byReference", since nothing is fetched separately or embedded +// as a compact JWT — see README.md for the resulting trade-off. +func (v *CredentialVerifier) createAuthenticationRequestUrlEncoded(base string, response_uri string, state string, clientId string, scope string, nonce string) (request string, err error) { + oidcClientId, err := resolveRedirectUriClientId(v.clientIdentification.Id, response_uri) + if err != nil { + return request, err + } + + values := url.Values{} + values.Set("response_type", "vp_token") + values.Set("response_mode", "direct_post") + values.Set("client_id", oidcClientId) + values.Set("response_uri", response_uri) + values.Set("state", state) + if nonce != "" { + values.Set("nonce", nonce) + } + + presentationDefinition, err := v.credentialsConfig.GetPresentationDefinition(clientId, scope) + if err != nil { + return request, err + } + if presentationDefinition != nil { + pdJSON, err := json.Marshal(presentationDefinition) + if err != nil { + return request, err + } + values.Set("presentation_definition", string(pdJSON)) + } + + dcql, err := v.credentialsConfig.GetDcqlQuery(clientId, scope) + if err != nil { + return request, err + } + if dcql != nil { + dcqlJSON, err := json.Marshal(dcql) + if err != nil { + return request, err + } + values.Set("dcql_query", string(dcqlJSON)) + } else { + logging.Log().Debugf("No dcql configured for %s - %s.", clientId, scope) + } + + return base + "?" + values.Encode(), nil +} + +// resolveRedirectUriClientId determines the client_id to send for the unsigned "redirect_uri" +// scheme. Per OIDC4VP, the wallet's only trust check for this scheme is that response_uri +// equals the URI embedded in client_id, so: +// - if no id is configured, it defaults to the actual response_uri, which is always consistent +// since both are derived from the same request. +// - if an id is configured, it must resolve to the same host as response_uri - a stale value +// (e.g. after an ingress hostname or pathPrefix change) would otherwise send a client_id the +// wallet is guaranteed to reject, instead of failing here with an actionable error. +// - any other id scheme (e.g. did:..., x509_san_dns:...) is left untouched. +func resolveRedirectUriClientId(configuredId string, responseUri string) (clientId string, err error) { + if configuredId == "" { + return CLIENT_ID_REDIRECT_URI_PREFIX + responseUri, nil + } + if !strings.HasPrefix(configuredId, CLIENT_ID_REDIRECT_URI_PREFIX) { + return configuredId, nil + } + + configuredUri := strings.TrimPrefix(configuredId, CLIENT_ID_REDIRECT_URI_PREFIX) + parsedConfigured, err := url.Parse(configuredUri) + if err != nil { + return clientId, err + } + parsedResponse, err := url.Parse(responseUri) + if err != nil { + return clientId, err + } + if parsedConfigured.Host != parsedResponse.Host { + logging.Log().Warnf("Configured redirect_uri client_id host %s does not match the request's response_uri host %s.", parsedConfigured.Host, parsedResponse.Host) + return clientId, ErrorClientIdHostMismatch + } + return configuredId, nil +} + func (v *CredentialVerifier) createAuthenticationRequestByValue(base string, response_uri string, state string, clientId string, scope string, nonce string) (request string, err error) { // We use a template to generate the final string @@ -1656,6 +1779,22 @@ func verifyConfig(verifierConfig *configModel.Verifier) error { if len(verifierConfig.SupportedModes) == 0 { return ErrorSupportedModesNotSet } + // The "default:" struct tag only applies when the config is loaded from server.yaml + // (via gookit/config); callers building configModel.Verifier directly (tests, or any + // future caller) get the zero value. Fall back here so behaviour matches the documented + // default regardless of how the config was constructed. Prefer "byReference" for backwards + // compatibility, but only when it is actually supported - otherwise fall back to whichever + // mode is supported, so a previously valid supportedModes-only config keeps booting. + if verifierConfig.RequestMode == "" { + if slices.Contains(verifierConfig.SupportedModes, REQUEST_MODE_BY_REFERENCE) { //nolint:govet + verifierConfig.RequestMode = REQUEST_MODE_BY_REFERENCE + } else { + verifierConfig.RequestMode = verifierConfig.SupportedModes[0] + } + } + if !slices.Contains(verifierConfig.SupportedModes, verifierConfig.RequestMode) { //nolint:govet + return ErrorRequestModeNotSupported + } return nil } @@ -1701,6 +1840,12 @@ func (v *CredentialVerifier) GetPathPrefix() string { return v.pathPrefix } +// GetRequestMode returns the request mode to use for flows where the caller has +// no way to request one explicitly (e.g. the OIDC-bridging authorization endpoint). +func (v *CredentialVerifier) GetRequestMode() string { + return v.fallbackRequestMode +} + // IsRefreshTokenEnabled reports whether the refresh token feature is active. func (v *CredentialVerifier) IsRefreshTokenEnabled() bool { return v.refreshTokenEnabled diff --git a/verifier/verifier_test.go b/verifier/verifier_test.go index 57d6d44..7499f48 100644 --- a/verifier/verifier_test.go +++ b/verifier/verifier_test.go @@ -9,6 +9,7 @@ import ( "encoding/base64" "encoding/pem" "errors" + "image/png" "math/big" "net/http" "net/url" @@ -46,17 +47,22 @@ func TestVerifyConfig(t *testing.T) { logging.Configure(LOGGING_CONFIG) type test struct { - testName string - configToTest configModel.Verifier - expectedError error + testName string + configToTest configModel.Verifier + expectedError error + expectedRequestMode string } 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", 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}, - {"If no validation mode is configured, verfication should fail.", configModel.Verifier{Did: "did:key:verifier", TirAddress: "http:tir.de", KeyAlgorithm: "RS256"}, ErrorUnsupportedValidationMode}, + {"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"}, RequestMode: "urlEncoded"}, nil, "urlEncoded"}, + {"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, ""}, + {"If no validation mode is configured, verfication should fail.", configModel.Verifier{Did: "did:key:verifier", TirAddress: "http:tir.de", KeyAlgorithm: "RS256"}, ErrorUnsupportedValidationMode, ""}, + {"If RequestMode is left empty and byReference is supported, it defaults to byReference and succeeds.", configModel.Verifier{Did: "did:key:verifier", TirAddress: "http:tir.de", ValidationMode: "none", KeyAlgorithm: "RS256", SupportedModes: []string{"byReference"}}, nil, "byReference"}, + {"If RequestMode is left empty and byReference is not supported, it falls back to the first supported mode and succeeds.", configModel.Verifier{Did: "did:key:verifier", TirAddress: "http:tir.de", ValidationMode: "none", KeyAlgorithm: "RS256", SupportedModes: []string{"urlEncoded"}}, nil, "urlEncoded"}, + {"If RequestMode is left empty and byReference is not supported but not the first entry either, it still falls back to the first supported mode and succeeds.", configModel.Verifier{Did: "did:key:verifier", TirAddress: "http:tir.de", ValidationMode: "none", KeyAlgorithm: "RS256", SupportedModes: []string{"urlEncoded", "byValue"}}, nil, "urlEncoded"}, + {"If RequestMode is set to a value outside SupportedModes, verification should fail.", configModel.Verifier{Did: "did:key:verifier", TirAddress: "http:tir.de", ValidationMode: "none", KeyAlgorithm: "RS256", SupportedModes: []string{"urlEncoded"}, RequestMode: "byValue"}, ErrorRequestModeNotSupported, "byValue"}, } for _, tc := range tests { @@ -67,6 +73,9 @@ func TestVerifyConfig(t *testing.T) { if verificationResult != tc.expectedError { t.Errorf("%s - Expected %v but was %v.", tc.testName, tc.expectedError, verificationResult) } + if tc.expectedRequestMode != "" && tc.configToTest.RequestMode != tc.expectedRequestMode { + t.Errorf("%s - Expected resolved RequestMode %v but was %v.", tc.testName, tc.expectedRequestMode, tc.configToTest.RequestMode) + } }) } @@ -270,7 +279,7 @@ func (msc *mockSessionCache) Delete(k string) { func (msc *mockSessionCache) GetWithExpiration(k string) (interface{}, time.Time, bool) { v, found := msc.sessions[k] - return v, <-time.After(5 * time.Second), found + return v, time.Time{}, found } func (mtc *mockTokenCache) Add(k string, x interface{}, d time.Duration) error { @@ -468,6 +477,212 @@ func TestStartSameDeviceFlow(t *testing.T) { } +// decodeQRPixelWidth extracts the width (in pixels) of a "data:image/png;base64,..." QR image, +// as returned by ReturnLoginQR/ReturnLoginQRV2. +func decodeQRPixelWidth(t *testing.T, dataUri string) int { + t.Helper() + encoded, found := strings.CutPrefix(dataUri, "data:image/png;base64,") + if !found { + t.Fatalf("Expected a data:image/png;base64, URI, got %s", dataUri) + } + raw, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + t.Fatalf("Was not able to base64-decode the QR image: %v", err) + } + img, err := png.Decode(strings.NewReader(string(raw))) + if err != nil { + t.Fatalf("Was not able to decode the QR PNG: %v", err) + } + return img.Bounds().Dx() +} + +// TestReturnLoginQRV2_ScalesWithContent guards against reintroducing a fixed-size QR canvas: +// a request with a large inlined dcql_query (as REQUEST_MODE_URL_ENCODED produces) needs far +// more QR modules than a bare request, and squeezing that into a fixed pixel size makes the +// QR unscannable (each module shrinks below what a phone camera can resolve). +func TestReturnLoginQRV2_ScalesWithContent(t *testing.T) { + logging.Configure(LOGGING_CONFIG) + + newVerifier := func(scopes map[string]map[string]configModel.ScopeEntry) CredentialVerifier { + return CredentialVerifier{ + host: "verifier.org", + did: "did:key:verifier", + sessionCache: &mockSessionCache{sessions: map[string]loginSession{}}, + nonceGenerator: &mockNonceGenerator{staticValues: []string{"randomNonce"}}, + tokenSigner: mockTokenSigner{}, + clock: mockClock{}, + credentialsConfig: mockCredentialConfig{mockScopes: scopes}, + supportedRequestModes: []string{REQUEST_MODE_URL_ENCODED}, + clientIdentification: configModel.ClientIdentification{Id: "redirect_uri:https://verifier.org/api/v1/authentication_response"}, + } + } + + small := newVerifier(createMockCredentials("", "", "", "", "", false)) + smallQr, err := small.ReturnLoginQRV2("verifier.org", "https", "https://wallet.example/callback", "small-state", "", "", "", REQUEST_MODE_URL_ENCODED) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + // Mirrors the shape of a real, moderately-sized DCQL query (a couple of credential + // format alternatives with a few claims each) - enough to meaningfully raise the QR + // version without exceeding a QR code's absolute capacity. + largeDcql := &configModel.DCQL{Credentials: make([]configModel.CredentialQuery, 3)} + for i := range largeDcql.Credentials { + largeDcql.Credentials[i] = configModel.CredentialQuery{ + Id: "some-fairly-long-credential-query-identifier-to-pad-things-out", + Claims: []configModel.ClaimsQuery{ + {Path: []interface{}{"firstName"}}, + {Path: []interface{}{"lastName"}}, + {Path: []interface{}{"roles"}}, + }, + } + } + large := newVerifier(map[string]map[string]configModel.ScopeEntry{"": {"": {DCQL: largeDcql}}}) + largeQr, err := large.ReturnLoginQRV2("verifier.org", "https", "https://wallet.example/callback", "large-state", "", "", "", REQUEST_MODE_URL_ENCODED) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + smallWidth := decodeQRPixelWidth(t, smallQr.QR) + largeWidth := decodeQRPixelWidth(t, largeQr.QR) + if largeWidth <= smallWidth { + t.Errorf("Expected the QR for the larger request to render wider than the smaller one, got small=%dpx large=%dpx", smallWidth, largeWidth) + } +} + +func TestStartSameDeviceFlow_UrlEncoded(t *testing.T) { + logging.Configure(LOGGING_CONFIG) + + sessionCache := mockSessionCache{sessions: map[string]loginSession{}} + nonceGenerator := mockNonceGenerator{staticValues: []string{"randomNonce"}} + credentialsConfig := mockCredentialConfig{createMockCredentials("", "", "", "", "", false), nil} + verifier := CredentialVerifier{ + host: "verifier.org", + did: "did:key:verifier", + sessionCache: &sessionCache, + nonceGenerator: &nonceGenerator, + tokenSigner: mockTokenSigner{}, + clock: mockClock{}, + credentialsConfig: credentialsConfig, + clientIdentification: configModel.ClientIdentification{Id: "redirect_uri:https://verifier.org/api/v1/authentication_response"}, + } + + authReq, err := verifier.StartSameDeviceFlow("verifier.org", "https", "my-random-session-id", "/redirect", "", "", REQUEST_MODE_URL_ENCODED, "", "") + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + base, query, found := strings.Cut(authReq, "?") + if !found { + t.Fatalf("Expected a query string in %s", authReq) + } + if base != "https://verifier.org/redirect" { + t.Errorf("Expected base https://verifier.org/redirect, got %s", base) + } + + values, err := url.ParseQuery(query) + if err != nil { + t.Fatalf("Was not able to parse the query string %s: %v", query, err) + } + assert.Equal(t, "vp_token", values.Get("response_type")) + assert.Equal(t, "direct_post", values.Get("response_mode")) + assert.Equal(t, "redirect_uri:https://verifier.org/api/v1/authentication_response", values.Get("client_id")) + assert.Equal(t, "https://verifier.org/api/v1/authentication_response", values.Get("response_uri")) + assert.Equal(t, "my-random-session-id", values.Get("state")) + assert.Equal(t, "randomNonce", values.Get("nonce")) + assert.Empty(t, values.Get("presentation_definition")) + assert.Empty(t, values.Get("dcql_query")) + + // no signed request object is generated/cached for this mode + cachedSession := sessionCache.sessions["my-random-session-id"] + assert.Empty(t, cachedSession.requestObject) +} + +func TestCreateAuthenticationRequestUrlEncoded_IncludesPresentationDefinitionAndDcql(t *testing.T) { + logging.Configure(LOGGING_CONFIG) + + pd := &configModel.PresentationDefinition{Id: "my-pd"} + requireHolderBinding := true + dcql := &configModel.DCQL{Credentials: []configModel.CredentialQuery{{Id: "my-cred", RequireCryptographicHolderBinding: &requireHolderBinding}}} + credentialsConfig := mockCredentialConfig{ + mockScopes: map[string]map[string]configModel.ScopeEntry{ + "my-client": {"my-scope": {PresentationDefinition: pd, DCQL: dcql}}, + }, + } + verifier := CredentialVerifier{ + credentialsConfig: credentialsConfig, + clientIdentification: configModel.ClientIdentification{Id: "redirect_uri:https://verifier.org/api/v1/authentication_response"}, + } + + authReq, err := verifier.createAuthenticationRequestUrlEncoded("openid4vp://", "https://verifier.org/api/v1/authentication_response", "my-state", "my-client", "my-scope", "my-nonce") + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + _, query, _ := strings.Cut(authReq, "?") + values, err := url.ParseQuery(query) + if err != nil { + t.Fatalf("Was not able to parse the query string %s: %v", query, err) + } + + var gotPd configModel.PresentationDefinition + if err := json.Unmarshal([]byte(values.Get("presentation_definition")), &gotPd); err != nil { + t.Fatalf("presentation_definition was not valid JSON: %v", err) + } + assert.Equal(t, *pd, gotPd) + + var gotDcql configModel.DCQL + if err := json.Unmarshal([]byte(values.Get("dcql_query")), &gotDcql); err != nil { + t.Fatalf("dcql_query was not valid JSON: %v", err) + } + assert.Equal(t, *dcql, gotDcql) +} + +func TestCreateAuthenticationRequestUrlEncoded_DefaultsClientIdWhenIdNotConfigured(t *testing.T) { + logging.Configure(LOGGING_CONFIG) + + credentialsConfig := mockCredentialConfig{createMockCredentials("", "", "", "", "", false), nil} + verifier := CredentialVerifier{credentialsConfig: credentialsConfig} + + authReq, err := verifier.createAuthenticationRequestUrlEncoded("openid4vp://", "https://verifier.org/api/v1/authentication_response", "state", "client", "scope", "nonce") + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + _, query, _ := strings.Cut(authReq, "?") + values, err := url.ParseQuery(query) + if err != nil { + t.Fatalf("Was not able to parse the query string %s: %v", query, err) + } + assert.Equal(t, "redirect_uri:https://verifier.org/api/v1/authentication_response", values.Get("client_id")) +} + +func TestCreateAuthenticationRequestUrlEncoded_RejectsClientIdHostMismatch(t *testing.T) { + logging.Configure(LOGGING_CONFIG) + + credentialsConfig := mockCredentialConfig{createMockCredentials("", "", "", "", "", false), nil} + verifier := CredentialVerifier{ + credentialsConfig: credentialsConfig, + clientIdentification: configModel.ClientIdentification{Id: "redirect_uri:https://old-verifier.org/api/v1/authentication_response"}, + } + + _, err := verifier.createAuthenticationRequestUrlEncoded("openid4vp://", "https://verifier.org/api/v1/authentication_response", "state", "client", "scope", "nonce") + if err != ErrorClientIdHostMismatch { + t.Errorf("Expected %v, got %v", ErrorClientIdHostMismatch, err) + } +} + +func TestCreateAuthenticationRequestUrlEncoded_PropagatesConfigError(t *testing.T) { + configError := errors.New("config_error") + credentialsConfig := mockCredentialConfig{mockError: configError} + verifier := CredentialVerifier{credentialsConfig: credentialsConfig, clientIdentification: configModel.ClientIdentification{Id: "redirect_uri:https://verifier.org/cb"}} + + _, err := verifier.createAuthenticationRequestUrlEncoded("openid4vp://", "https://verifier.org/cb", "state", "client", "scope", "nonce") + if err != configError { + t.Errorf("Expected %v, got %v", configError, err) + } +} + // extractResponseUri decodes the (unsigned) JWT payload embedded in a // REQUEST_MODE_BY_VALUE authentication request and returns its response_uri claim. func extractResponseUri(t *testing.T, authRequest string) string { @@ -790,14 +1005,14 @@ 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", GenerateKey: true, SupportedModes: []string{"urlEncoded"}}}, nil}, + {"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", GenerateKey: true, SupportedModes: []string{"urlEncoded"}, RequestMode: "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"}}}, ErrorInvalidKeyConfig}, + {"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"}, RequestMode: "urlEncoded"}}, ErrorInvalidKeyConfig}, {"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}, - {"KID should be added if the key does not contain it and a KID value is configured", configModel.Configuration{Verifier: configModel.Verifier{Did: "did:key:verifier", TirAddress: "https://tir.org", ValidationMode: "none", SessionExpiry: 30, KeyAlgorithm: "RS256", GenerateKey: false, SupportedModes: []string{"urlEncoded"}, KeyPath: keyPath, ClientIdentification: configModel.ClientIdentification{Kid: "random-kid"}}}, nil}, - {"ClientID should be added to the key when KID value and config are missing", configModel.Configuration{Verifier: configModel.Verifier{Did: "did:key:verifier", TirAddress: "https://tir.org", ValidationMode: "none", SessionExpiry: 30, KeyAlgorithm: "RS256", GenerateKey: false, SupportedModes: []string{"urlEncoded"}, KeyPath: keyPath, ClientIdentification: configModel.ClientIdentification{Id: "client-id-value"}}}, nil}, + {"KID should be added if the key does not contain it and a KID value is configured", configModel.Configuration{Verifier: configModel.Verifier{Did: "did:key:verifier", TirAddress: "https://tir.org", ValidationMode: "none", SessionExpiry: 30, KeyAlgorithm: "RS256", GenerateKey: false, SupportedModes: []string{"urlEncoded"}, RequestMode: "urlEncoded", KeyPath: keyPath, ClientIdentification: configModel.ClientIdentification{Kid: "random-kid"}}}, nil}, + {"ClientID should be added to the key when KID value and config are missing", configModel.Configuration{Verifier: configModel.Verifier{Did: "did:key:verifier", TirAddress: "https://tir.org", ValidationMode: "none", SessionExpiry: 30, KeyAlgorithm: "RS256", GenerateKey: false, SupportedModes: []string{"urlEncoded"}, RequestMode: "urlEncoded", KeyPath: keyPath, ClientIdentification: configModel.ClientIdentification{Id: "client-id-value"}}}, nil}, } for _, tc := range tests { @@ -1962,6 +2177,7 @@ func TestInitVerifier_CredentialStatusWiring(t *testing.T) { KeyAlgorithm: "RS256", GenerateKey: true, SupportedModes: []string{"urlEncoded"}, + RequestMode: "urlEncoded", } type test struct {