Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d8cef07
Read Presentation Definition from local PDP backend
reinkrul Nov 28, 2025
eeb783a
build Docker image for branch
reinkrul Dec 1, 2025
811fc85
build docker image
reinkrul Dec 1, 2025
262a774
basic testing setup
reinkrul Dec 2, 2025
921821f
Issue mandaatcredential
reinkrul Dec 4, 2025
19f5960
#3953: add support for urn:ietf:params:oauth:grant-type:jwt-bearer fo…
reinkrul Dec 4, 2025
18d8023
Relax did:x509 certificate key usage validation
reinkrul Dec 16, 2025
dccb5ed
Enable RS256 support
reinkrul Dec 16, 2025
206e5e1
Add AortaGtK CA certs to OS trust bundle
reinkrul Dec 16, 2025
219635f
Add EV intermediate CA to trusted certs
reinkrul Dec 16, 2025
12f6e9e
Don't send presentation_submission
reinkrul Dec 16, 2025
14358d9
Introduce policy_id parameter
reinkrul Dec 16, 2025
708ad5a
Try to marshal VPs as JWT, not JSON-LD
reinkrul Dec 16, 2025
89527d3
Updated README
reinkrul Dec 16, 2025
f07d0f6
Updated README
reinkrul Dec 16, 2025
6254059
test for VP type
reinkrul Dec 16, 2025
68a5e21
write vps to temp file
reinkrul Dec 17, 2025
2572c09
revert VC JWT fix
reinkrul Dec 17, 2025
64cc71f
set fixed key ID
reinkrul Dec 17, 2025
724051f
fix vp.type to array
reinkrul Dec 17, 2025
50625ab
Made token response parsing lenient
reinkrul Dec 17, 2025
9bd652e
Reverted jwt ID
reinkrul Dec 17, 2025
1465cee
Merge branch 'master' into lspxnuts
reinkrul Jan 27, 2026
dac8036
#3980: Support validation of DeziIDTokenCredential
reinkrul Feb 2, 2026
2ebea32
implemented e2e test
reinkrul Feb 2, 2026
6552dfa
Update vcr/credential/validator.go
reinkrul Feb 2, 2026
ea7ffac
cleanup
reinkrul Feb 2, 2026
ea905dc
Merge branch 'lspxnuts' into project-gf
reinkrul Feb 2, 2026
ed58bc1
Merge branch 'iss3980-validate-idtoken-credential' into project-gf
reinkrul Feb 2, 2026
ca4005d
Push docker image
reinkrul Feb 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/build-images.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ on:
push:
branches:
- master
- project-gf
tags:
- 'v*'
pull_request:
branches:
- master
- project-gf

# cancel build action if superseded by new commit on same branch
concurrency:
Expand Down Expand Up @@ -51,7 +53,7 @@ jobs:
images: nutsfoundation/nuts-node
tags: |
# generate 'master' tag for the master branch
type=ref,event=branch,enable={{is_default_branch}},prefix=
type=ref,event=branch,enable=true,prefix=
# generate 5.2.1 tag
type=semver,pattern={{version}}
flavor: |
Expand Down
5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
COPY go.sum .
RUN go mod download && go mod verify

COPY . .

Check warning on line 20 in Dockerfile

View workflow job for this annotation

GitHub Actions / docker

Attempting to Copy file that is excluded by .dockerignore

CopyIgnoredFile: Attempting to Copy file "." that is excluded by .dockerignore More info: https://docs.docker.com/go/dockerfile/rule/copy-ignored-file/

Check warning on line 20 in Dockerfile

View workflow job for this annotation

GitHub Actions / e2e-test

Attempting to Copy file that is excluded by .dockerignore

CopyIgnoredFile: Attempting to Copy file "." that is excluded by .dockerignore More info: https://docs.docker.com/go/dockerfile/rule/copy-ignored-file/

Check warning on line 20 in Dockerfile

View workflow job for this annotation

GitHub Actions / docker

Attempting to Copy file that is excluded by .dockerignore

CopyIgnoredFile: Attempting to Copy file "." that is excluded by .dockerignore More info: https://docs.docker.com/go/dockerfile/rule/copy-ignored-file/
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags="-w -s -X 'github.com/nuts-foundation/nuts-node/core.GitCommit=${GIT_COMMIT}' -X 'github.com/nuts-foundation/nuts-node/core.GitBranch=${GIT_BRANCH}' -X 'github.com/nuts-foundation/nuts-node/core.GitVersion=${GIT_VERSION}'" -o /opt/nuts/nuts

# alpine
Expand All @@ -25,7 +25,10 @@
RUN apk update \
&& apk add --no-cache \
tzdata \
curl
curl \
ca-certificates
COPY pki/cacerts/* /usr/local/share/ca-certificates/
RUN update-ca-certificates
COPY --from=builder /opt/nuts/nuts /usr/bin/nuts

HEALTHCHECK --start-period=30s --timeout=5s --interval=10s \
Expand Down
16 changes: 16 additions & 0 deletions LSPxNuts_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# LSPxNuts Proof of Concept

This is a branch that for the Proof of Concept of the LSPxNuts project.

It adds or alters the following functionality versus the mainstream Nuts node:

- OAuth2 `vp_bearer` token exchange: read presentation definition from local definitions instead of fetching it from the remote authorization server.
LSP doesn't support presentation definitions, meaning that we need to look it up locally.
- Add support for JWT bearer grant type. If the server supports this, it uses this grant type instead of the Nuts-specific vp_token-bearer grant type.
- Add CA certificates of Sectigo (root CA, OV and EV intermediate CA) to Docker image's OS CA bundle, because they're used by AORTA-LSP.
- Fix marshalling of Verifiable Presentations in JWT format; `type` was marshalled as JSON-LD (single-entry-array was replaced by string)
- Add `policy_id` field to access token request to specify the Presentation Definition that should be used.
The `scope` can then be specified as whatever the use case requires (e.g. SMART on FHIR-esque scopes).
- Relax `did:x509` key usage check: the certificate from UZI smart cards that is used to sign credentials, doesn't have `serverAuth` key usage, only `digitalSignature`.
This broke, since we didn't specify the key usage, but `x509.Verify()` expects key usage `serverAuth` to be present by default.
- Add support for `RS256` (RSA 2048) signatures, since that's what UZI smart cards produce.
21 changes: 18 additions & 3 deletions auth/api/iam/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,16 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/nuts-foundation/nuts-node/core/to"
"html/template"
"net/http"
"net/url"
"slices"
"strings"
"time"

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

"github.com/labstack/echo/v4"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jwt"
Expand Down Expand Up @@ -750,9 +752,18 @@ func (r Wrapper) RequestServiceAccessToken(ctx context.Context, request RequestS
if request.Body.Credentials != nil {
credentials = *request.Body.Credentials
}

if request.Body.IdToken != nil {
idTokenCredential, err := credential.CreateDeziIDTokenCredential(*request.Body.IdToken)
if err != nil {
return nil, core.InvalidInputError("failed to create id_token credential: %w", err)
}
credentials = append(credentials, *idTokenCredential)
}

// assert that self-asserted credentials do not contain an issuer or credentialSubject.id. These values must be set
// by the nuts-node to build the correct wallet for a DID. See https://github.com/nuts-foundation/nuts-node/issues/3696
// As a sideeffect it is no longer possible to pass signed credentials to this API.
// As a side effect it is no longer possible to pass signed credentials to this API.
for _, cred := range credentials {
var credentialSubject []map[string]interface{}
if err := cred.UnmarshalCredentialSubject(&credentialSubject); err != nil {
Expand All @@ -774,7 +785,11 @@ func (r Wrapper) RequestServiceAccessToken(ctx context.Context, request RequestS
useDPoP = false
}
clientID := r.subjectToBaseURL(request.SubjectID)
tokenResult, err := r.auth.IAMClient().RequestRFC021AccessToken(ctx, clientID.String(), request.SubjectID, request.Body.AuthorizationServer, request.Body.Scope, useDPoP, credentials)
var policyId string
if request.Body.PolicyId != nil {
policyId = *request.Body.PolicyId
}
tokenResult, err := r.auth.IAMClient().RequestRFC021AccessToken(ctx, clientID.String(), request.SubjectID, request.Body.AuthorizationServer, request.Body.Scope, policyId, useDPoP, credentials)
if err != nil {
// this can be an internal server error, a 400 oauth error or a 412 precondition failed if the wallet does not contain the required credentials
return nil, err
Expand Down
18 changes: 9 additions & 9 deletions auth/api/iam/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -873,7 +873,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) {
request := RequestServiceAccessTokenRequestObject{SubjectID: holderSubjectID, Body: body}
request.Params.CacheControl = to.Ptr("no-cache")
// Initial call to populate cache
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil).Return(response, nil).Times(2)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", "", true, nil).Return(response, nil).Times(2)
token, err := ctx.client.RequestServiceAccessToken(nil, request)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Expand Down
11 changes: 11 additions & 0 deletions auth/api/iam/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 10 additions & 6 deletions auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ package auth
import (
"crypto/tls"
"errors"
"net/url"
"path"
"slices"
"time"

"github.com/nuts-foundation/nuts-node/auth/client/iam"
"github.com/nuts-foundation/nuts-node/policy"
"github.com/nuts-foundation/nuts-node/vdr"
"github.com/nuts-foundation/nuts-node/vdr/didjwk"
"github.com/nuts-foundation/nuts-node/vdr/didkey"
Expand All @@ -30,10 +36,6 @@ import (
"github.com/nuts-foundation/nuts-node/vdr/didweb"
"github.com/nuts-foundation/nuts-node/vdr/didx509"
"github.com/nuts-foundation/nuts-node/vdr/resolver"
"net/url"
"path"
"slices"
"time"

"github.com/nuts-foundation/nuts-node/auth/services"
"github.com/nuts-foundation/nuts-node/auth/services/notary"
Expand All @@ -58,6 +60,7 @@ type Auth struct {
relyingParty oauth.RelyingParty
contractNotary services.ContractNotary
serviceResolver didman.CompoundServiceResolver
policyBackend policy.PDPBackend
keyStore crypto.KeyStore
vcr vcr.VCR
pkiProvider pki.Provider
Expand Down Expand Up @@ -100,12 +103,13 @@ func (auth *Auth) ContractNotary() services.ContractNotary {

// NewAuthInstance accepts a Config with several Nuts Engines and returns an instance of Auth
func NewAuthInstance(config Config, vdrInstance vdr.VDR, subjectManager didsubject.Manager, vcr vcr.VCR, keyStore crypto.KeyStore,
serviceResolver didman.CompoundServiceResolver, jsonldManager jsonld.JSONLD, pkiProvider pki.Provider) *Auth {
serviceResolver didman.CompoundServiceResolver, jsonldManager jsonld.JSONLD, pkiProvider pki.Provider, policyBackend policy.PDPBackend) *Auth {
return &Auth{
config: config,
jsonldManager: jsonldManager,
vdrInstance: vdrInstance,
subjectManager: subjectManager,
policyBackend: policyBackend,
keyStore: keyStore,
vcr: vcr,
pkiProvider: pkiProvider,
Expand All @@ -126,7 +130,7 @@ func (auth *Auth) RelyingParty() oauth.RelyingParty {

func (auth *Auth) IAMClient() iam.Client {
keyResolver := resolver.DIDKeyResolver{Resolver: auth.vdrInstance.Resolver()}
return iam.NewClient(auth.vcr.Wallet(), keyResolver, auth.subjectManager, auth.keyStore, auth.jsonldManager.DocumentLoader(), auth.strictMode, auth.httpClientTimeout)
return iam.NewClient(auth.vcr.Wallet(), keyResolver, auth.subjectManager, auth.keyStore, auth.jsonldManager.DocumentLoader(), auth.policyBackend, auth.strictMode, auth.httpClientTimeout)
}

// Configure the Auth struct by creating a validator and create an Irma server
Expand Down
9 changes: 5 additions & 4 deletions auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
package auth

import (
"testing"

"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/crypto"
"github.com/nuts-foundation/nuts-node/jsonld"
Expand All @@ -28,7 +30,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"testing"
)

func TestAuth_Configure(t *testing.T) {
Expand All @@ -47,7 +48,7 @@ func TestAuth_Configure(t *testing.T) {
vdrInstance := vdr.NewMockVDR(ctrl)
vdrInstance.EXPECT().Resolver().AnyTimes()

i := NewAuthInstance(config, vdrInstance, nil, vcr.NewTestVCRInstance(t), crypto.NewMemoryCryptoInstance(t), nil, nil, pkiMock)
i := NewAuthInstance(config, vdrInstance, nil, vcr.NewTestVCRInstance(t), crypto.NewMemoryCryptoInstance(t), nil, nil, pkiMock, nil)

require.NoError(t, i.Configure(tlsServerConfig))
})
Expand All @@ -61,7 +62,7 @@ func TestAuth_Configure(t *testing.T) {
vdrInstance := vdr.NewMockVDR(ctrl)
vdrInstance.EXPECT().Resolver().AnyTimes()

i := NewAuthInstance(config, vdrInstance, nil, vcr.NewTestVCRInstance(t), crypto.NewMemoryCryptoInstance(t), nil, nil, pkiMock)
i := NewAuthInstance(config, vdrInstance, nil, vcr.NewTestVCRInstance(t), crypto.NewMemoryCryptoInstance(t), nil, nil, pkiMock, nil)

require.NoError(t, i.Configure(tlsServerConfig))
})
Expand Down Expand Up @@ -119,7 +120,7 @@ func TestAuth_IAMClient(t *testing.T) {
vdrInstance := vdr.NewMockVDR(ctrl)
vdrInstance.EXPECT().Resolver().AnyTimes()

i := NewAuthInstance(config, vdrInstance, nil, vcr.NewTestVCRInstance(t), crypto.NewMemoryCryptoInstance(t), nil, jsonld.NewTestJSONLDManager(t), pkiMock)
i := NewAuthInstance(config, vdrInstance, nil, vcr.NewTestVCRInstance(t), crypto.NewMemoryCryptoInstance(t), nil, jsonld.NewTestJSONLDManager(t), pkiMock, nil)

assert.NotNil(t, i.IAMClient())
})
Expand Down
56 changes: 51 additions & 5 deletions auth/client/iam/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,17 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/lestrrat-go/jwx/v2/jws"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/nuts-foundation/nuts-node/crypto"
"github.com/nuts-foundation/nuts-node/vdr/resolver"
"io"
"net/http"
"net/url"
"strings"
"time"

"github.com/lestrrat-go/jwx/v2/jws"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/nuts-foundation/nuts-node/crypto"
"github.com/nuts-foundation/nuts-node/vdr/resolver"

"github.com/nuts-foundation/go-did/vc"
"github.com/nuts-foundation/nuts-node/auth/log"
"github.com/nuts-foundation/nuts-node/auth/oauth"
Expand Down Expand Up @@ -205,21 +206,66 @@ func (hb HTTPClient) AccessToken(ctx context.Context, tokenEndpoint string, data
return token, oauthError
}

// TODO: Remove this when Itzos fixed their Token Response
type LenientTokenResponse struct {
AccessToken string `json:"access_token"`
DPoPKid *string `json:"dpop_kid,omitempty"`
ExpiresAt *any `json:"expires_at,omitempty"`
ExpiresIn *any `json:"expires_in,omitempty"`
TokenType string `json:"token_type"`
Scope *string `json:"scope,omitempty"`
}

var responseData []byte
if responseData, err = io.ReadAll(response.Body); err != nil {
return token, fmt.Errorf("unable to read response: %w", err)
}
if err = json.Unmarshal(responseData, &token); err != nil {
var lenientToken LenientTokenResponse
if err = json.Unmarshal(responseData, &lenientToken); err != nil {
// Cut off the response body to 100 characters max to prevent logging of large responses
responseBodyString := string(responseData)
if len(responseBodyString) > core.HttpResponseBodyLogClipAt {
responseBodyString = responseBodyString[:core.HttpResponseBodyLogClipAt] + "...(clipped)"
}
return token, fmt.Errorf("unable to unmarshal response: %w, %s", err, responseBodyString)
}
token.AccessToken = lenientToken.AccessToken
token.DPoPKid = lenientToken.DPoPKid
token.TokenType = lenientToken.TokenType
token.Scope = lenientToken.Scope
token.ExpiresAt, err = toInt(lenientToken.ExpiresAt)
if err != nil {
return token, fmt.Errorf("unable to parse expires_at: %w", err)
}
token.ExpiresIn, err = toInt(lenientToken.ExpiresIn)
if err != nil {
return token, fmt.Errorf("unable to parse expires_in: %w", err)
}

return token, nil
}

func toInt(value *any) (*int, error) {
// handle expires_in which can be int or string
if value == nil {
return nil, nil
}
switch v := (*value).(type) {
case float64:
intValue := int(v)
return &intValue, nil
case string:
var intValue int
_, err := fmt.Sscanf(v, "%d", &intValue)
if err != nil {
return nil, fmt.Errorf("unable to parse string to int: %w", err)
}
return &intValue, nil
default:
return nil, fmt.Errorf("unable to parse value of type %T to int", v)
}
}

// PostError posts an OAuth error to the redirect URL and returns the redirect URL with the error as query parameter.
func (hb HTTPClient) PostError(ctx context.Context, err oauth.OAuth2Error, verifierCallbackURL url.URL) (string, error) {
// initiate http client, create a POST request with x-www-form-urlencoded body and send it to the redirect URL
Expand Down
Loading
Loading